pax_global_header 0000666 0000000 0000000 00000000064 14711665223 0014521 g ustar 00root root 0000000 0000000 52 comment=654f07ae08c1c5e9bfe700abcdaed258d1fcc059
python-didl-lite-1.4.1/ 0000775 0000000 0000000 00000000000 14711665223 0014712 5 ustar 00root root 0000000 0000000 python-didl-lite-1.4.1/.coveragerc 0000664 0000000 0000000 00000000024 14711665223 0017027 0 ustar 00root root 0000000 0000000 [run]
branch = True
python-didl-lite-1.4.1/.github/ 0000775 0000000 0000000 00000000000 14711665223 0016252 5 ustar 00root root 0000000 0000000 python-didl-lite-1.4.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14711665223 0020307 5 ustar 00root root 0000000 0000000 python-didl-lite-1.4.1/.github/workflows/ci-cd.yml 0000664 0000000 0000000 00000007226 14711665223 0022020 0 ustar 00root root 0000000 0000000 name: Build
on:
- push
- pull_request
env:
publish-python-version: 3.12
jobs:
markdown_lint:
name: Markdown Lint
runs-on: ubuntu-latest
steps:
- uses: DavidAnson/markdownlint-cli2-action@v16
with:
globs: "**/*.md"
lint_test_build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade build
python -m pip install --upgrade tox tox-gh-actions
- name: Test with tox
run: tox
- name: Build package
run: python -m build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: python-package-distributions-${{ matrix.python-version }}
path: dist/
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
if: ${{ hashFiles('coverage-py312.xml') != '' }}
with:
files: coverage-py312.xml
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
publish-to-pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
needs:
- markdown_lint
- lint_test_build
environment:
name: pypi
url: https://pypi.org/p/python-didl-lite
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions-${{ env.publish-python-version }}
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
github-release:
needs:
- publish-to-pypi
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions-${{ env.publish-python-version }}
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v2.1.1
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create
'${{ github.ref_name }}'
--repo '${{ github.repository }}'
--notes ""
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release upload
'${{ github.ref_name }}' dist/**
--repo '${{ github.repository }}'
publish-to-testpypi:
name: Publish to TestPyPI
runs-on: ubuntu-latest
if: github.repository == 'StevenLooman/python-didl-lite'
needs:
- markdown_lint
- lint_test_build
environment:
name: testpypi
url: https://test.pypi.org/p/python-didl-lite
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions-${{ env.publish-python-version }}
path: dist/
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
continue-on-error: true
python-didl-lite-1.4.1/.gitignore 0000664 0000000 0000000 00000000176 14711665223 0016706 0 ustar 00root root 0000000 0000000 *.pyc
*.egg*
.cache/
.coverage
.idea/
.mypy_cache/
.pytest_cache/
.tox/
coverage*.xml
.venv/
__pycache__
build/
dist/
cov.xml
python-didl-lite-1.4.1/.pre-commit-config.yaml 0000664 0000000 0000000 00000002371 14711665223 0021176 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 'v2.3.0'
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: '21.7b0'
hooks:
- id: black
args:
- --safe
- --quiet
files: ^(async_upnp_client|tests)/.+\.py$
- repo: https://github.com/codespell-project/codespell
rev: 'v2.1.0'
hooks:
- id: codespell
args:
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types: [csv, json]
exclude: ^tests/fixtures/
- repo: https://gitlab.com/pycqa/flake8
rev: '3.9.2'
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.6.0
- flake8-noqa==1.1.0
- pydocstyle==6.1.1
files: ^(async_upnp_client|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
rev: '5.9.2'
hooks:
- id: isort
args:
- --profile=black
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.910'
hooks:
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies:
- aiohttp>=3.7.4
- pytest~=6.2.4
files: ^(async_upnp_client|tests)/.+\.py$
python-didl-lite-1.4.1/.travis.yml 0000664 0000000 0000000 00000000750 14711665223 0017025 0 ustar 00root root 0000000 0000000 language: python
os: linux
dist: focal
jobs:
fast_finish: true
include:
- python: "3.6"
env: TOXENV=py36
- python: "3.7"
env: TOXENV=py37
- python: "3.8"
env: TOXENV=py38
- python: "3.9"
env: TOXENV=py39
- python: "3.9"
env: TOXENV=flake8
- python: "3.9"
env: TOXENV=pylint
- python: "3.9"
env: TOXENV=typing
- python: "3.9"
env: TOXENV=black
install: pip install -U tox
script: tox
services:
- docker
python-didl-lite-1.4.1/CHANGES.rst 0000664 0000000 0000000 00000003454 14711665223 0016522 0 ustar 00root root 0000000 0000000 Changes
=======
1.4.1 (2024-11-03)
- Cater for malformed XML from WiiM pro in non-strict mode (#14, @pp81381)
- Add Python 3.13 support
1.4.0 (2023-12-16)
- Treat upnp:class as case-insensitive when in non-strict mode (@chishm)
- Raise a `DidlLiteException` when upnp:class is invalid when in strict mode (@chishm)
- Drop Python3.6 and Python3.7 support, add Python3.10, Python3.11, Python3.12 support
- Bump development dependencies
1.3.2 (2021-11-29)
- Annotate DidlObject properties that mypy doesn't see in __init__ (@chishm)
1.3.1 (2021-10-25)
- Class properties from ContentDirectory:4 (@chishm)
1.3.0 (2021-10-07)
- `DidlObject.resources` is now deprecated, use `DidlObject.res` instead
- Rename `utils.ns_tag` to `utils.expand_namespace_tag`
- Rename `utils.namespace_tag` to `utils.split_namespace_tag`
- Add `__repr__` methods to DIDL classes for easier debugging
- Allow camelCase to get and set DIDL object properties (@chishm)
1.2.6 (2021-03-04)
- Add non-strict option misbehaving devices
1.2.5 (2020-09-12)
- Save xml element for addition information
1.2.4 (2019-03-20)
- Better namespace naming
1.2.3 (2019-01-27)
- Fix infinite recursion error in DidlObject.__getattr__/Resource.__getattr__
1.2.2 (2019-01-26)
- Add `py.typed` to support PEP 561
- MyPy understands DidlObject/Descriptor have dynamic properties
- Move utility methods to utils
1.2.1 (2019-01-20)
- Typing fixes (@scop)
- Skip unknown object types on parse (@scop)
- Use defusedxml to parse XML (@scop)
1.2.0 (2018-11-03)
- Typing fixes (@scop)
- Allow unknown properties to be parsed and stored, such as albumArtURI on Items, as used by Kodi
1.1.0 (2018-08-17)
- Always set properties, even if no value was given
1.0.1 (2018-06-29)
- Use default ("") for id and parent_id
1.0.0 (2018-06-29)
- Initial release
python-didl-lite-1.4.1/LICENSE.md 0000664 0000000 0000000 00000024360 14711665223 0016323 0 ustar 00root root 0000000 0000000 Apache License
==============
_Version 2.0, January 2004_
_<>_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
python-didl-lite-1.4.1/README.rst 0000664 0000000 0000000 00000001472 14711665223 0016405 0 ustar 00root root 0000000 0000000 DIDL-Lite (Digital Item Declaration Language) tools for Python
==============================================================
DIDL-Lite tools for Python to read and write DIDL-Lite-xml.
Usage
-----
See ``tests/`` for examples on how to use this.
Note that this package does **NOT** do any type checking. Coercion of data types from and to strings must be done by the user of this library.
Resources / documents
---------------------
DIDL-Lite resources and documents:
* `UPnP-av-ContentDirectory-v1-Service `_
* `UPnP-av-ContentDirectory-v2-Service `_
* `didl-lite-v2.xsd `_
* `mpeg21-didl `_
python-didl-lite-1.4.1/didl_lite/ 0000775 0000000 0000000 00000000000 14711665223 0016643 5 ustar 00root root 0000000 0000000 python-didl-lite-1.4.1/didl_lite/__init__.py 0000664 0000000 0000000 00000000215 14711665223 0020752 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""DIDL-Lite (Digital Item Declaration Language) tools for Python."""
from didl_lite import didl_lite # noqa: F401
python-didl-lite-1.4.1/didl_lite/didl_lite.py 0000664 0000000 0000000 00000103640 14711665223 0021152 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""DIDL-Lite (Digital Item Declaration Language) tools for Python."""
# pylint: disable=too-many-lines
from typing import (
Any,
Dict,
Iterable,
List,
Mapping,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
)
from xml.etree import ElementTree as ET
import defusedxml.ElementTree
from .utils import (
NAMESPACES,
didl_property_def_key,
didl_property_key,
expand_namespace_tag,
split_namespace_tag,
to_camel_case,
)
TDO = TypeVar("TDO", bound="DidlObject") # pylint: disable=invalid-name
TC = TypeVar("TC", bound="Container") # pylint: disable=invalid-name
TD = TypeVar("TD", bound="Descriptor") # pylint: disable=invalid-name
TR = TypeVar("TR", bound="Resource") # pylint: disable=invalid-name
class DidlLiteException(Exception):
"""DIDL Lite Exception."""
# region: DidlObjects
# upnp_class to python type mapping
_upnp_class_map: Dict[str, Type["DidlObject"]] = {}
_upnp_class_map_lowercase: Dict[str, Type["DidlObject"]] = {}
class DidlObject:
"""DIDL Object."""
tag: Optional[str] = None
upnp_class: str = "object"
didl_properties_defs: List[Tuple[str, str, str]] = [
("didl_lite", "@id", "R"),
("didl_lite", "@parentID", "R"),
("didl_lite", "@restricted", "R"),
("dc", "title", "R"),
("upnp", "class", "R"),
("dc", "creator", "O"),
("didl_lite", "res", "O"),
("upnp", "writeStatus", "O"),
]
id: str
parent_id: str
res: List["Resource"]
xml_el: Optional[ET.Element]
descriptors: Sequence["Descriptor"]
@classmethod
def __init_subclass__(cls: Type["DidlObject"], **kwargs: Any) -> None:
"""Create mapping of upnp_class to Python type for fast lookup."""
super().__init_subclass__(**kwargs)
assert cls.upnp_class not in _upnp_class_map
assert cls.upnp_class.lower() not in _upnp_class_map_lowercase
_upnp_class_map[cls.upnp_class] = cls
_upnp_class_map_lowercase[cls.upnp_class.lower()] = cls
def __init__(
self,
id: str = "",
parent_id: str = "",
descriptors: Optional[Sequence["Descriptor"]] = None,
xml_el: Optional[ET.Element] = None,
strict: bool = True,
**properties: Any,
) -> None:
"""Initialize."""
# pylint: disable=invalid-name,redefined-builtin,too-many-arguments
properties["id"] = id
properties["parent_id"] = parent_id
properties["class"] = self.upnp_class
properties["res"] = properties.get("res") or properties.get("resources") or []
if "resources" in properties:
del properties["resources"]
self._ensure_required_properties(strict, properties)
self._set_property_defaults()
self._set_properties(properties)
self.xml_el = xml_el
self.descriptors = descriptors if descriptors else []
def _ensure_required_properties(
self, strict: bool, properties: Mapping[str, Any]
) -> None:
"""Check if all required properties are given."""
if not strict:
return
python_property_keys = {didl_property_key(key) for key in properties}
for property_def in self.didl_properties_defs:
key = didl_property_def_key(property_def)
if property_def[2] == "R" and key not in python_property_keys:
raise DidlLiteException(key + " is mandatory")
def _set_property_defaults(self) -> None:
"""Ensure we have default/known slots, and set them all to None."""
for property_def in self.didl_properties_defs:
key = didl_property_def_key(property_def)
setattr(self, key, None)
def _set_properties(self, properties: Mapping[str, Any]) -> None:
"""Set attributes from properties."""
for key, value in properties.items():
setattr(self, key, value)
@classmethod
def from_xml(cls: Type[TDO], xml_el: ET.Element, strict: bool = True) -> TDO:
"""
Initialize from an XML node.
I.e., parse XML and return instance.
"""
# pylint: disable=too-many-locals
properties = {} # type: Dict[str, Any]
# attributes
for attr_key, attr_value in xml_el.attrib.items():
key = to_camel_case(attr_key)
properties[key] = attr_value
# child-nodes
for xml_child_node in xml_el:
if xml_child_node.tag == expand_namespace_tag("didl_lite:res"):
continue
_, tag = split_namespace_tag(xml_child_node.tag)
key = to_camel_case(tag)
value = xml_child_node.text
properties[key] = value
# attributes of child nodes
parent_key = key
for attr_key, attr_value in xml_child_node.attrib.items():
key = parent_key + "_" + to_camel_case(attr_key)
properties[key] = attr_value
# resources
resources = []
for res_el in xml_el.findall("./didl_lite:res", NAMESPACES):
resource = Resource.from_xml(res_el)
resources.append(resource)
properties["res"] = properties["resources"] = resources
# descriptors
descriptors = []
for desc_el in xml_el.findall("./didl_lite:desc", NAMESPACES):
descriptor = Descriptor.from_xml(desc_el)
descriptors.append(descriptor)
return cls(xml_el=xml_el, descriptors=descriptors, strict=strict, **properties)
def to_xml(self) -> ET.Element:
"""Convert self to XML Element."""
assert self.tag is not None
item_el = ET.Element(self.tag)
elements = {"": item_el}
# properties
for property_def in self.didl_properties_defs:
if "@" in property_def[1]:
continue
key = didl_property_def_key(property_def)
if (
getattr(self, key) is None or key == "res"
): # no resources, handled later on
continue
tag = property_def[0] + ":" + property_def[1]
property_el = ET.Element(tag, {})
property_el.text = getattr(self, key)
item_el.append(property_el)
elements[property_def[1]] = property_el
# attributes and property@attributes
for property_def in self.didl_properties_defs:
if "@" not in property_def[1]:
continue
key = didl_property_def_key(property_def)
value = getattr(self, key)
if value is None:
continue
el_name, attr_name = property_def[1].split("@")
property_el = elements[el_name]
property_el.attrib[attr_name] = value
# resource
for resource in self.resources:
res_el = resource.to_xml()
item_el.append(res_el)
# descriptor
for descriptor in self.descriptors:
desc_el = descriptor.to_xml()
item_el.append(desc_el)
return item_el
def __getattr__(self, name: str) -> Any:
"""Get attribute, modifying case as needed."""
if name == "resources":
return getattr(self, "res")
if name in self.__dict__:
return self.__dict__[name]
cleaned_name = didl_property_key(name)
if cleaned_name not in self.__dict__:
raise AttributeError(name)
return self.__dict__[cleaned_name]
def __setattr__(self, name: str, value: Any) -> None:
"""Set attribute, modifying case as needed."""
if name not in self.__dict__:
# Redirect to the lower_camel_case version if it's already set,
# which is the case for all defined didl properties.
cleaned_name = didl_property_key(name)
if cleaned_name in self.__dict__:
name = cleaned_name
self.__dict__[name] = value
def __repr__(self) -> str:
"""Evaluatable string representation of this object."""
class_name = type(self).__name__
attr = ", ".join(
f"{key}={val!r}"
for key, val in self.__dict__.items()
if key not in ("class", "xml_el")
)
return f"{class_name}({attr})"
# region: items
class Item(DidlObject):
"""DIDL Item."""
# pylint: disable=too-few-public-methods
tag = "item"
upnp_class = "object.item"
didl_properties_defs = DidlObject.didl_properties_defs + [
("didl_lite", "@refID", "O"), # actually, R, but ignore for now
("upnp", "bookmarkID", "O"),
]
class ImageItem(Item):
"""DIDL Image Item."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.imageItem"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "longDescription", "O"),
("upnp", "storageMedium", "O"),
("upnp", "rating", "O"),
("dc", "description", "O"),
("dc", "publisher", "O"),
("dc", "date", "O"),
("dc", "rights", "O"),
]
class Photo(ImageItem):
"""DIDL Photo."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.imageItem.photo"
didl_properties_defs = ImageItem.didl_properties_defs + [
("upnp", "album", "O"),
]
class AudioItem(Item):
"""DIDL Audio Item."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.audioItem"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "genre", "O"),
("dc", "description", "O"),
("upnp", "longDescription", "O"),
("dc", "publisher", "O"),
("dc", "language", "O"),
("dc", "relation", "O"),
("dc", "rights", "O"),
]
class MusicTrack(AudioItem):
"""DIDL Music Track."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.audioItem.musicTrack"
didl_properties_defs = AudioItem.didl_properties_defs + [
("upnp", "artist", "O"),
("upnp", "album", "O"),
("upnp", "originalTrackNumber", "O"),
("upnp", "playlist", "O"),
("upnp", "storageMedium", "O"),
("dc", "contributor", "O"),
("dc", "date", "O"),
]
class AudioBroadcast(AudioItem):
"""DIDL Audio Broadcast."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.audioItem.audioBroadcast"
didl_properties_defs = AudioItem.didl_properties_defs + [
("upnp", "region", "O"),
("upnp", "radioCallSign", "O"),
("upnp", "radioStationID", "O"),
("upnp", "radioBand", "O"),
("upnp", "channelNr", "O"),
("upnp", "signalStrength", "O"),
("upnp", "signalLocked", "O"),
("upnp", "tuned", "O"),
("upnp", "recordable", "O"),
]
class AudioBook(AudioItem):
"""DIDL Audio Book."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.audioItem.audioBook"
didl_properties_defs = AudioItem.didl_properties_defs + [
("upnp", "storageMedium", "O"),
("upnp", "producer", "O"),
("dc", "contributor", "O"),
("dc", "date", "O"),
]
class VideoItem(Item):
"""DIDL Video Item."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.videoItem"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "genre", "O"),
("upnp", "genre@id", "O"),
("upnp", "genre@type", "O"),
("upnp", "longDescription", "O"),
("upnp", "producer", "O"),
("upnp", "rating", "O"),
("upnp", "actor", "O"),
("upnp", "director", "O"),
("dc", "description", "O"),
("dc", "publisher", "O"),
("dc", "language", "O"),
("dc", "relation", "O"),
("upnp", "playbackCount", "O"),
("upnp", "lastPlaybackTime", "O"),
("upnp", "lastPlaybackPosition", "O"),
("upnp", "recordedDayOfWeek", "O"),
("upnp", "srsRecordScheduleID", "O"),
]
class Movie(VideoItem):
"""DIDL Movie."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.videoItem.movie"
didl_properties_defs = VideoItem.didl_properties_defs + [
("upnp", "storageMedium", "O"),
("upnp", "DVDRegionCode", "O"),
("upnp", "channelName", "O"),
("upnp", "scheduledStartTime", "O"),
("upnp", "scheduledEndTime", "O"),
("upnp", "programTitle", "O"),
("upnp", "seriesTitle", "O"),
("upnp", "episodeCount", "O"),
("upnp", "episodeNumber", "O"),
("upnp", "episodeSeason", "O"),
]
class VideoBroadcast(VideoItem):
"""DIDL Video Broadcast."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.videoItem.videoBroadcast"
didl_properties_defs = VideoItem.didl_properties_defs + [
("upnp", "icon", "O"),
("upnp", "region", "O"),
("upnp", "channelNr", "O"),
("upnp", "signalStrength", "O"),
("upnp", "signalLocked", "O"),
("upnp", "tuned", "O"),
("upnp", "recordable", "O"),
("upnp", "callSign", "O"),
("upnp", "price", "O"),
("upnp", "payPerView", "O"),
]
class MusicVideoClip(VideoItem):
"""DIDL Music Video Clip."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.videoItem.musicVideoClip"
didl_properties_defs = VideoItem.didl_properties_defs + [
("upnp", "artist", "O"),
("upnp", "storageMedium", "O"),
("upnp", "album", "O"),
("upnp", "scheduledStartTime", "O"),
("upnp", "scheduledStopTime", "O"),
# ('upnp', 'director', 'O'), # duplicate in standard
("dc", "contributor", "O"),
("dc", "date", "O"),
]
class PlaylistItem(Item):
"""DIDL Playlist Item."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.playlistItem"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "artist", "O"),
("upnp", "genre", "O"),
("upnp", "longDescription", "O"),
("upnp", "storageMedium", "O"),
("dc", "description", "O"),
("dc", "date", "O"),
("dc", "language", "O"),
]
class TextItem(Item):
"""DIDL Text Item."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.textItem"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "author", "O"),
("upnp", "res@protection", "O"),
("upnp", "longDescription", "O"),
("upnp", "storageMedium", "O"),
("upnp", "rating", "O"),
("dc", "description", "O"),
("dc", "publisher", "O"),
("dc", "contributor", "O"),
("dc", "date", "O"),
("dc", "relation", "O"),
("dc", "language", "O"),
("dc", "rights", "O"),
]
class BookmarkItem(Item):
"""DIDL Bookmark Item."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.bookmarkItem"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "bookmarkedObjectID", "R"),
("upnp", "neverPlayable", "O"),
("upnp", "deviceUDN", "R"),
("upnp", "serviceType", "R"),
("upnp", "serviceId", "R"),
("dc", "date", "O"),
("dc", "stateVariableCollection", "R"),
]
class EpgItem(Item):
"""DIDL EPG Item."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.epgItem"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "channelGroupName", "O"),
("upnp", "channelGroupName@id", "O"),
("upnp", "epgProviderName", "O"),
("upnp", "serviceProvider", "O"),
("upnp", "channelName", "O"),
("upnp", "channelNr", "O"),
("upnp", "programTitle", "O"),
("upnp", "seriesTitle", "O"),
("upnp", "programID", "O"),
("upnp", "programID@type", "O"),
("upnp", "seriesID", "O"),
("upnp", "seriesID@type", "O"),
("upnp", "channelID", "O"),
("upnp", "channelID@type", "O"),
("upnp", "channelID@distriNetworkName", "O"),
("upnp", "channelID@distriNetworkID", "O"),
("upnp", "episodeType", "O"),
("upnp", "episodeCount", "O"),
("upnp", "episodeNumber", "O"),
("upnp", "episodeSeason", "O"),
("upnp", "programCode", "O"),
("upnp", "programCode@type", "O"),
("upnp", "rating", "O"),
("upnp", "rating@type", "O"),
("upnp", "rating@advice", "O"),
("upnp", "rating@equivalentAge", "O"),
("upnp", "recommendationID", "O"),
("upnp", "recommendationID@type", "O"),
("upnp", "genre", "O"),
("upnp", "genre@id", "O"),
("upnp", "genre@extended", "O"),
("upnp", "artist", "O"),
("upnp", "artist@role", "O"),
("upnp", "actor", "O"),
("upnp", "actor@role", "O"),
("upnp", "author", "O"),
("upnp", "author@role", "O"),
("upnp", "producer", "O"),
("upnp", "director", "O"),
("dc", "publisher", "O"),
("dc", "contributor", "O"),
("upnp", "callSign", "O"),
("upnp", "networkAffiliation", "O"),
# ('upnp', 'serviceProvider', 'O'), # duplicate in standard
("upnp", "price", "O"),
("upnp", "price@currency", "O"),
("upnp", "payPerView", "O"),
# ('upnp', 'epgProviderName', 'O'), # duplicate in standard
("dc", "description", "O"),
("upnp", "longDescription", "O"),
("upnp", "icon", "O"),
("upnp", "region", "O"),
("upnp", "rights", "O"),
("dc", "language", "O"),
("dc", "relation", "O"),
("upnp", "scheduledStartTime", "O"),
("upnp", "scheduledEndTime", "O"),
("upnp", "recordable", "O"),
("upnp", "foreignMetadata", "O"),
]
class AudioProgram(EpgItem):
"""DIDL Audio Program."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.epgItem.audioProgram"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "radioCallSign", "O"),
("upnp", "radioStationID", "O"),
("upnp", "radioBand", "O"),
]
class VideoProgram(EpgItem):
"""DIDL Video Program."""
# pylint: disable=too-few-public-methods
upnp_class = "object.item.epgItem.videoProgram"
didl_properties_defs = Item.didl_properties_defs + [
("upnp", "price", "O"),
("upnp", "price@currency", "O"),
("upnp", "payPerView", "O"),
]
# endregion
# region: containers
class Container(DidlObject, list):
"""DIDL Container."""
# pylint: disable=too-few-public-methods
tag = "container"
upnp_class = "object.container"
didl_properties_defs = DidlObject.didl_properties_defs + [
("didl_lite", "@childCount", "O"),
("upnp", "createClass", "O"),
("upnp", "searchClass", "O"),
("didl_lite", "@searchable", "O"),
("didl_lite", "@neverPlayable", "O"),
]
def __init__(
self,
id: str = "",
parent_id: str = "",
descriptors: Optional[Sequence["Descriptor"]] = None,
xml_el: Optional[ET.Element] = None,
strict: bool = True,
children: Iterable[DidlObject] = (),
**properties: Any,
) -> None:
"""Initialize."""
# pylint: disable=redefined-builtin,too-many-arguments
super().__init__(id, parent_id, descriptors, xml_el, strict, **properties)
self.extend(children)
@classmethod
def from_xml(cls: Type[TC], xml_el: ET.Element, strict: bool = True) -> TC:
"""
Initialize from an XML node.
I.e., parse XML and return instance.
"""
instance = super().from_xml(xml_el, strict)
# add all children
didl_objects = from_xml_el(xml_el, strict)
instance.extend(didl_objects) # pylint: disable=no-member
return instance
def to_xml(self) -> ET.Element:
"""Convert self to XML Element."""
container_el = super().to_xml()
for didl_object in self:
didl_object_el = didl_object.to_xml()
container_el.append(didl_object_el)
return container_el
def __repr__(self) -> str:
"""Evaluatable string representation of this object."""
class_name = type(self).__name__
attr = ", ".join(
f"{key}={val!r}"
for key, val in self.__dict__.items()
if key not in ("class", "xml_el")
)
children_repr = ", ".join(repr(child) for child in self)
return f"{class_name}({attr}, children=[{children_repr}])"
class Person(Container):
"""DIDL Person."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.person"
didl_properties_defs = Container.didl_properties_defs + [
("dc", "language", "O"),
]
class MusicArtist(Person):
"""DIDL Music Artist."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.person.musicArtist"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "genre", "O"),
("upnp", "artistDiscographyURI", "O"),
]
class PlaylistContainer(Container):
"""DIDL Playlist Container."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.playlistContainer"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "artist", "O"),
("upnp", "genre", "O"),
("upnp", "longDescription", "O"),
("upnp", "producer", "O"),
("upnp", "storageMedium", "O"),
("dc", "description", "O"),
("dc", "contributor", "O"),
("dc", "date", "O"),
("dc", "language", "O"),
("dc", "rights", "O"),
]
class Album(Container):
"""DIDL Album."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.album"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "storageMedium", "O"),
("dc", "longDescription", "O"),
("dc", "description", "O"),
("dc", "publisher", "O"),
("dc", "contributor", "O"),
("dc", "date", "O"),
("dc", "relation", "O"),
("dc", "rights", "O"),
]
class MusicAlbum(Album):
"""DIDL Music Album."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.album.musicAlbum"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "artist", "O"),
("upnp", "genre", "O"),
("upnp", "producer", "O"),
("upnp", "albumArtURI", "O"),
("upnp", "toc", "O"),
]
class PhotoAlbum(Album):
"""DIDL Photo Album."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.album.photoAlbum"
didl_properties_defs = Container.didl_properties_defs + []
class Genre(Container):
"""DIDL Genre."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.genre"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "genre", "O"),
("upnp", "longDescription", "O"),
("dc", "description", "O"),
]
class MusicGenre(Genre):
"""DIDL Music Genre."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.genre.musicGenre"
didl_properties_defs = Container.didl_properties_defs + []
class MovieGenre(Genre):
"""DIDL Movie Genre."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.genre.movieGenre"
didl_properties_defs = Container.didl_properties_defs + []
class ChannelGroup(Container):
"""DIDL Channel Group."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.channelGroup"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "channelGroupName", "O"),
("upnp", "channelGroupName@id", "O"),
("upnp", "epgProviderName", "O"),
("upnp", "serviceProvider", "O"),
("upnp", "icon", "O"),
("upnp", "region", "O"),
]
class AudioChannelGroup(ChannelGroup):
"""DIDL Audio Channel Group."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.channelGroup.audioChannelGroup"
didl_properties_defs = Container.didl_properties_defs + []
class VideoChannelGroup(ChannelGroup):
"""DIDL Video Channel Group."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.channelGroup.videoChannelGroup"
didl_properties_defs = Container.didl_properties_defs + []
class EpgContainer(Container):
"""DIDL EPG Container."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.epgContainer"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "channelGroupName", "O"),
("upnp", "channelGroupName@id", "O"),
("upnp", "epgProviderName", "O"),
("upnp", "serviceProvider", "O"),
("upnp", "channelName", "O"),
("upnp", "channelNr", "O"),
("upnp", "channelID", "O"),
("upnp", "channelID@type", "O"),
("upnp", "radioCallSign", "O"),
("upnp", "radioStationID", "O"),
("upnp", "radioBand", "O"),
("upnp", "callSign", "O"),
("upnp", "networkAffiliation", "O"),
# ('upnp', 'serviceProvider', 'O'), # duplicate in standard
("upnp", "price", "O"),
("upnp", "price@currency", "O"),
("upnp", "payPerView", "O"),
# ('upnp', 'epgProviderName', 'O'), # duplicate in standard
("upnp", "icon", "O"),
("upnp", "region", "O"),
("dc", "language", "O"),
("dc", "relation", "O"),
("upnp", "dateTimeRange", "O"),
]
class StorageSystem(Container):
"""DIDL Storage System."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.storageSystem"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "storageTotal", "R"),
("upnp", "storageUsed", "R"),
("upnp", "storageFree", "R"),
("upnp", "storageMaxPartition", "R"),
("upnp", "storageMedium", "R"),
]
class StorageVolume(Container):
"""DIDL Storage Volume."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.storageVolume"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "storageTotal", "R"),
("upnp", "storageUsed", "R"),
("upnp", "storageFree", "R"),
("upnp", "storageMedium", "R"),
]
class StorageFolder(Container):
"""DIDL Storage Folder."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.storageFolder"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "storageUsed", "R"),
]
class BookmarkFolder(Container):
"""DIDL Bookmark Folder."""
# pylint: disable=too-few-public-methods
upnp_class = "object.container.bookmarkFolder"
didl_properties_defs = Container.didl_properties_defs + [
("upnp", "genre", "O"),
("upnp", "longDescription", "O"),
("dc", "description", "O"),
]
# endregion
class Resource:
"""DIDL Resource."""
# pylint: disable=too-few-public-methods,too-many-instance-attributes
def __init__(
self,
uri: Optional[str],
protocol_info: Optional[str],
import_uri: Optional[str] = None,
size: Optional[str] = None,
duration: Optional[str] = None,
bitrate: Optional[str] = None,
sample_frequency: Optional[str] = None,
bits_per_sample: Optional[str] = None,
nr_audio_channels: Optional[str] = None,
resolution: Optional[str] = None,
color_depth: Optional[str] = None,
protection: Optional[str] = None,
xml_el: Optional[ET.Element] = None,
) -> None:
"""Initialize."""
# pylint: disable=too-many-arguments
self.uri = uri
self.protocol_info = protocol_info
self.import_uri = import_uri
self.size = size
self.duration = duration
self.bitrate = bitrate
self.sample_frequency = sample_frequency
self.bits_per_sample = bits_per_sample
self.nr_audio_channels = nr_audio_channels
self.resolution = resolution
self.color_depth = color_depth
self.protection = protection
self.xml_el = xml_el
@classmethod
def from_xml(cls: Type[TR], xml_el: ET.Element) -> TR:
"""Initialize from an XML node."""
uri = xml_el.text
protocol_info = xml_el.attrib.get("protocolInfo")
import_uri = xml_el.attrib.get("importUri")
size = xml_el.attrib.get("size")
duration = xml_el.attrib.get("duration")
bitrate = xml_el.attrib.get("bitrate")
sample_frequency = xml_el.attrib.get("sampleFrequency")
bits_per_sample = xml_el.attrib.get("bitsPerSample")
nr_audio_channels = xml_el.attrib.get("nrAudioChannels")
resolution = xml_el.attrib.get("resolution")
color_depth = xml_el.attrib.get("colorDepth")
protection = xml_el.attrib.get("protection")
return cls(
uri,
protocol_info=protocol_info,
import_uri=import_uri,
size=size,
duration=duration,
bitrate=bitrate,
sample_frequency=sample_frequency,
bits_per_sample=bits_per_sample,
nr_audio_channels=nr_audio_channels,
resolution=resolution,
color_depth=color_depth,
protection=protection,
xml_el=xml_el,
)
def to_xml(self) -> ET.Element:
"""Convert self to XML."""
attribs = {
"protocolInfo": self.protocol_info or "",
}
res_el = ET.Element("res", attribs)
res_el.text = self.uri
return res_el
def __repr__(self) -> str:
"""Evaluatable string representation of this object."""
class_name = type(self).__name__
attr = ", ".join(
f"{key}={val!r}"
for key, val in self.__dict__.items()
if val is not None and key != "xml_el"
)
return f"{class_name}({attr})"
class Descriptor:
"""DIDL Descriptor."""
def __init__(
self,
id: str,
name_space: str,
type: Optional[str] = None,
text: Optional[str] = None,
xml_el: Optional[ET.Element] = None,
) -> None:
"""Initialize."""
# pylint: disable=invalid-name,redefined-builtin,too-many-arguments
self.id = id
self.name_space = name_space
self.type = type
self.text = text
self.xml_el = xml_el
@classmethod
def from_xml(cls: Type[TD], xml_el: ET.Element) -> TD:
"""Initialize from an XML node."""
id_ = xml_el.attrib["id"]
name_space = xml_el.attrib["nameSpace"]
type_ = xml_el.attrib.get("type")
text = xml_el.text
return cls(id_, name_space, type=type_, text=text, xml_el=xml_el)
def to_xml(self) -> ET.Element:
"""Convert self to XML."""
attribs = {
"id": self.id,
"nameSpace": self.name_space,
}
if self.type is not None:
attribs["type"] = self.type
desc_el = ET.Element("desc", attribs)
desc_el.text = self.text
return desc_el
def __getattr__(self, name: str) -> Any:
"""Get attribute."""
if name not in self.__dict__:
raise AttributeError(name)
return self.__dict__[name]
def __repr__(self) -> str:
"""Evaluatable string representation of this object."""
class_name = type(self).__name__
attr = ", ".join(
f"{key}={val!r}"
for key, val in self.__dict__.items()
if val is not None and key != "xml_el"
)
return f"{class_name}({attr})"
# endregion
def to_xml_string(*objects: DidlObject) -> bytes:
"""Convert items to DIDL-Lite XML string."""
root_el = ET.Element("DIDL-Lite", {})
root_el.attrib["xmlns"] = NAMESPACES["didl_lite"]
root_el.attrib["xmlns:dc"] = NAMESPACES["dc"]
root_el.attrib["xmlns:upnp"] = NAMESPACES["upnp"]
root_el.attrib["xmlns:sec"] = NAMESPACES["sec"]
for didl_object in objects:
didl_object_el = didl_object.to_xml()
root_el.append(didl_object_el)
return ET.tostring(root_el)
def from_xml_string(
xml_string: str, strict: bool = True
) -> List[Union[DidlObject, Descriptor]]:
"""Convert XML string to DIDL Objects."""
xml_el = defusedxml.ElementTree.fromstring(xml_string)
return from_xml_el(xml_el, strict)
def from_xml_el(
xml_el: ET.Element, strict: bool = True
) -> List[Union[DidlObject, Descriptor]]:
"""Convert XML Element to DIDL Objects."""
didl_objects = [] # type: List[Union[DidlObject, Descriptor]]
# items and containers, in order
for child_el in xml_el:
if child_el.tag != expand_namespace_tag(
"didl_lite:item"
) and child_el.tag != expand_namespace_tag("didl_lite:container"):
continue
# construct item
upnp_class = child_el.find("./upnp:class", NAMESPACES)
if upnp_class is None or not upnp_class.text:
if strict:
continue
# WiiM Pro and possibly other Linkplay devices emit
# upnp_class above the item element instead of inside it
upnp_class = xml_el.find("./upnp:class", NAMESPACES)
if upnp_class is None or not upnp_class.text:
continue
didl_object_type = type_by_upnp_class(upnp_class.text, strict)
if didl_object_type is None:
if strict:
raise DidlLiteException(f"upnp:class {upnp_class.text} is unknown")
continue
didl_object = didl_object_type.from_xml(child_el, strict)
didl_objects.append(didl_object)
# descriptors
for desc_el in xml_el.findall("./didl_lite:desc", NAMESPACES):
desc = Descriptor.from_xml(desc_el)
didl_objects.append(desc)
return didl_objects
# upnp_class to python type mapping
def type_by_upnp_class(
upnp_class: str, strict: bool = True
) -> Optional[Type[DidlObject]]:
"""Get DidlObject-type by upnp_class.
When strict is False, the upnp_class lookup will be done ignoring string
case.
"""
if strict:
return _upnp_class_map.get(upnp_class)
return _upnp_class_map_lowercase.get(upnp_class.lower())
python-didl-lite-1.4.1/didl_lite/py.typed 0000664 0000000 0000000 00000000000 14711665223 0020330 0 ustar 00root root 0000000 0000000 python-didl-lite-1.4.1/didl_lite/utils.py 0000664 0000000 0000000 00000004136 14711665223 0020361 0 ustar 00root root 0000000 0000000 """Utilities."""
import re
from typing import Optional, Tuple
NAMESPACES = {
"didl_lite": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
"dc": "http://purl.org/dc/elements/1.1/",
"sec": "http://www.sec.co.kr/",
"upnp": "urn:schemas-upnp-org:metadata-1-0/upnp/",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
def expand_namespace_tag(tag: str) -> str:
"""
Expand namespace-alias to url.
E.g.,
_ns_tag('didl_lite:item') -> '{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}item'
"""
if ":" not in tag:
return tag
namespace, tag = tag.split(":")
namespace_uri = NAMESPACES[namespace]
return f"{{{namespace_uri}}}{tag}"
def split_namespace_tag(namespaced_tag: str) -> Tuple[Optional[str], str]:
"""
Extract namespace and tag from namespaced-tag.
E.g., _namespace_tag('{urn:schemas-upnp-org:metadata-1-0/upnp/}class') ->
'urn:schemas-upnp-org:metadata-1-0/upnp/', 'class'
"""
if "}" not in namespaced_tag:
return None, namespaced_tag
idx = namespaced_tag.index("}")
namespace = namespaced_tag[1:idx]
tag = namespaced_tag[idx + 1 :] # noqa: E203
return namespace, tag
def to_camel_case(name: str) -> str:
"""Get camel case of name."""
sub1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", sub1).lower()
def didl_property_key(didl_property_name: str) -> str:
"""Get Python property key for a DIDL property name."""
if ":" in didl_property_name:
# Remove the namespace from the property name
didl_property_name = didl_property_name.partition(":")[2]
if didl_property_name.startswith("@"):
return to_camel_case(didl_property_name.replace("@", ""))
return to_camel_case(didl_property_name.replace("@", "_"))
def didl_property_def_key(didl_property_def: Tuple[str, ...]) -> str:
"""Get Python property key for didl_property_def."""
if didl_property_def[1].startswith("@"):
return to_camel_case(didl_property_def[1].replace("@", ""))
return to_camel_case(didl_property_def[1].replace("@", "_"))
python-didl-lite-1.4.1/pylintrc 0000664 0000000 0000000 00000000055 14711665223 0016501 0 ustar 00root root 0000000 0000000 [BASIC]
good-names=otherItem, storageMedium
python-didl-lite-1.4.1/setup.cfg 0000664 0000000 0000000 00000000643 14711665223 0016536 0 ustar 00root root 0000000 0000000 [bdist_wheel]
python-tag=py3
[metadata]
license_file = LICENSE.md
[flake8]
max-line-length = 99
[mypy]
check_untyped_defs = true
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true
python-didl-lite-1.4.1/setup.py 0000775 0000000 0000000 00000002632 14711665223 0016432 0 ustar 00root root 0000000 0000000 """Setup."""
import os.path
from setuptools import setup
here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, "README.rst"), encoding="utf-8") as f:
LONG_DESCRIPTION = f.read()
with open(os.path.join(here, "README.rst"), encoding="utf-8") as f:
DESCRIPTION = f.readline().strip()
INSTALL_REQUIRES = [
"defusedxml>=0.6.0",
]
TEST_REQUIRES = [
"pytest~=7.4.3",
]
setup(
name="python-didl-lite",
version="1.4.1",
description=DESCRIPTION,
long_description=LONG_DESCRIPTION,
long_description_content_type="text/x-rst",
url="https://github.com/StevenLooman/python-didl-lite",
author="Steven Looman",
author_email="steven.looman@gmail.com",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
],
packages=["didl_lite"],
package_data={
"didl_lite": ["py.typed"],
},
python_requires=">=3.8",
install_requires=INSTALL_REQUIRES,
tests_require=TEST_REQUIRES,
)
python-didl-lite-1.4.1/tests/ 0000775 0000000 0000000 00000000000 14711665223 0016054 5 ustar 00root root 0000000 0000000 python-didl-lite-1.4.1/tests/test_didl_lite.py 0000664 0000000 0000000 00000063211 14711665223 0021421 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""Unit tests for didl_lite."""
import pytest
from defusedxml import ElementTree as ET
from didl_lite import didl_lite
NAMESPACES = {
"didl_lite": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
"dc": "http://purl.org/dc/elements/1.1/",
"upnp": "urn:schemas-upnp-org:metadata-1-0/upnp/",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
class TestDidlLite:
"""Tests for didl_lite."""
# pylint: disable=too-many-public-methods
def test_item_from_xml(self) -> None:
"""Test item from XML."""
didl_string = """
-
Audio Item Title
object.item.audioItem
English
url
Long description
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
item = items[0]
assert item.xml_el is not None
assert getattr(item, "title") == "Audio Item Title"
assert getattr(item, "upnp_class") == "object.item.audioItem"
assert getattr(item, "language") == "English"
assert getattr(item, "longDescription") == "Long description"
assert getattr(item, "long_description") == "Long description"
assert isinstance(item, didl_lite.AudioItem)
assert not hasattr(item, "non_existing")
resources = item.res
assert len(resources) == 1
resource = resources[0]
assert resource.xml_el is not None
assert resource.protocol_info == "protocol_info"
assert resource.uri == "url"
assert not hasattr(item, "non_existing")
assert item.res == item.resources
def test_item_bad_class(self) -> None:
"""Test item from XML that has a badly-cased upnp:class."""
didl_string = """
-
Audio Item Title
Object.Item.AudioItem
English
url
Long description
"""
with pytest.raises(
didl_lite.DidlLiteException,
match="upnp:class Object.Item.AudioItem is unknown",
):
didl_lite.from_xml_string(didl_string)
items = didl_lite.from_xml_string(didl_string, strict=False)
assert len(items) == 1
item = items[0]
assert isinstance(item, didl_lite.AudioItem)
def test_item_from_xml_not_strict(self) -> None:
"""Test item from XML."""
didl_string = """
-
Audio Item Title
object.item.audioItem
English
url
"""
items = didl_lite.from_xml_string(didl_string, strict=False)
assert len(items) == 1
item = items[0]
assert item.xml_el is not None
assert getattr(item, "title") == "Audio Item Title"
assert getattr(item, "upnp_class") == "object.item.audioItem"
assert getattr(item, "language") == "English"
assert isinstance(item, didl_lite.AudioItem)
assert not hasattr(item, "non_existing")
resources = item.res
assert len(resources) == 1
resource = resources[0]
assert resource.xml_el is not None
assert resource.protocol_info is None # This is now allowed with strict=False
assert resource.uri == "url"
assert not hasattr(item, "non_existing")
assert item.res == item.resources
def test_item_to_xml(self) -> None:
"""Test item to XML."""
resource = didl_lite.Resource("url", "protocol_info")
items = [
didl_lite.AudioItem(
id="0",
parent_id="0",
title="Audio Item Title",
restricted="1",
resources=[resource],
language="English",
longDescription="Long description",
),
]
didl_string = didl_lite.to_xml_string(*items).decode("utf-8")
assert 'xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"' in didl_string
assert 'xmlns:dc="http://purl.org/dc/elements/1.1/"' in didl_string
assert 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"' in didl_string
assert 'xmlns:sec="http://www.sec.co.kr/"' in didl_string
assert 'xmlns:ns1="urn:schemas-upnp-org:metadata-1-0/upnp/"' not in didl_string
didl_el = ET.fromstring(didl_string)
item_el = didl_el.find("./didl_lite:item", NAMESPACES)
assert item_el is not None
assert item_el.attrib["id"] == "0"
assert item_el.attrib["parentID"] == "0"
assert item_el.attrib["restricted"] == "1"
title_el = item_el.find("./dc:title", NAMESPACES)
assert title_el is not None
assert title_el.text == "Audio Item Title"
class_el = item_el.find("./upnp:class", NAMESPACES)
assert class_el is not None
assert class_el.text == "object.item.audioItem"
language_el = item_el.find("./dc:language", NAMESPACES)
assert language_el is not None
assert language_el.text == "English"
long_description_el = item_el.find("./upnp:longDescription", NAMESPACES)
assert long_description_el is not None
assert long_description_el.text == "Long description"
res_el = item_el.find("./didl_lite:res", NAMESPACES)
assert res_el is not None
assert res_el.attrib["protocolInfo"] == "protocol_info"
assert res_el.text == "url"
def test_item_repr(self) -> None:
"""Test item's repr can convert back to an equivalent item."""
# pylint: disable=import-outside-toplevel
# repr method doesn't know how package was imported, so only uses class names
from didl_lite.didl_lite import AudioItem, Resource
item = AudioItem(
id="0",
parent_id="0",
title="Audio Item Title",
restricted="1",
res=[
Resource("url", "protocol_info"),
Resource("url2", "protocol_info2"),
],
language="English",
)
item_repr = repr(item)
item_remade = eval(item_repr) # pylint: disable=eval-used
assert ET.tostring(item.to_xml()) == ET.tostring(item_remade.to_xml())
def test_container_from_xml(self) -> None:
"""Test container from XML."""
didl_string = """
Album Container Title
object.container.album
-
Audio Item Title
object.item.audioItem
English
url
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
container = items[0]
assert container.xml_el is not None
assert isinstance(container, didl_lite.Container)
assert getattr(container, "title") == "Album Container Title"
assert getattr(container, "upnp_class") == "object.container.album"
item = container[0]
assert item.xml_el is not None
assert item.title == "Audio Item Title"
assert item.upnp_class == "object.item.audioItem"
assert item.language == "English"
resources = item.res
assert len(resources) == 1
resource = resources[0]
assert resource.xml_el is not None
assert resource.protocol_info == "protocol_info"
assert resource.uri == "url"
assert item.res == item.resources
def test_container_to_xml(self) -> None:
"""Test container to XML."""
container = didl_lite.Album(
id="0", parent_id="0", title="Audio Item Title", restricted="1"
)
resource = didl_lite.Resource("url", "protocol_info")
item = didl_lite.AudioItem(
id="0",
parent_id="0",
title="Audio Item Title",
restricted="1",
resources=[resource],
language="English",
)
container.append(item)
didl_string = didl_lite.to_xml_string(container).decode("utf-8")
didl_el = ET.fromstring(didl_string)
container_el = didl_el.find("./didl_lite:container", NAMESPACES)
assert container_el is not None
assert container_el.attrib["id"] == "0"
assert container_el.attrib["parentID"] == "0"
assert container_el.attrib["restricted"] == "1"
item_el = container_el.find("./didl_lite:item", NAMESPACES)
assert item_el is not None
assert item_el.attrib["id"] == "0"
assert item_el.attrib["parentID"] == "0"
assert item_el.attrib["restricted"] == "1"
title_el = item_el.find("./dc:title", NAMESPACES)
assert title_el is not None
assert title_el.text == "Audio Item Title"
class_el = item_el.find("./upnp:class", NAMESPACES)
assert class_el is not None
assert class_el.text == "object.item.audioItem"
language_el = item_el.find("./dc:language", NAMESPACES)
assert language_el is not None
assert language_el.text == "English"
res_el = item_el.find("./didl_lite:res", NAMESPACES)
assert res_el is not None
assert res_el.attrib["protocolInfo"] == "protocol_info"
assert res_el.text == "url"
def test_container_repr(self) -> None:
"""Test containers's repr can convert back to an equivalent container."""
# pylint: disable=import-outside-toplevel
from didl_lite.didl_lite import Album, AudioItem, Resource
container = Album(
id="0", parent_id="0", title="Audio Item Title", restricted="1"
)
resource = Resource("url", "protocol_info")
item = AudioItem(
id="0",
parent_id="0",
title="Audio Item Title",
restricted="1",
resources=[resource],
language="English",
)
container.append(item)
container_repr = repr(container)
container_remade = eval(container_repr) # pylint: disable=eval-used
assert ET.tostring(container.to_xml()) == ET.tostring(container_remade.to_xml())
def test_descriptor_from_xml_root(self) -> None:
"""Test root descriptor from XML."""
didl_string = """
Text
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
descriptor = items[0]
assert descriptor is not None
assert descriptor.xml_el is not None
assert getattr(descriptor, "id") == "1"
assert getattr(descriptor, "name_space") == "ns"
assert getattr(descriptor, "type") == "type"
assert getattr(descriptor, "text") == "Text"
def test_descriptor_from_xml_item(self) -> None:
"""Test item descriptor from XML."""
didl_string = """
-
Audio Item Title
object.item.audioItem
English
url
Text
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
item = items[0]
assert item is not None
assert isinstance(item, didl_lite.AudioItem)
descriptor = item.descriptors[0]
assert descriptor is not None
assert descriptor.xml_el is not None
assert descriptor.id == "1"
assert descriptor.name_space == "ns"
assert descriptor.type == "type"
assert descriptor.text == "Text"
def test_descriptor_from_xml_container(self) -> None:
"""Test container descriptor from XML."""
didl_string = """
Album Container Title
object.container.album
Text
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
container = items[0]
assert container is not None
assert container.xml_el is not None
assert isinstance(container, didl_lite.Container)
descriptor = container.descriptors[0]
assert descriptor is not None
assert descriptor.xml_el is not None
assert descriptor.id == "1"
assert descriptor.name_space == "ns"
assert descriptor.type == "type"
assert descriptor.text == "Text"
def test_descriptor_from_xml_container_item(self) -> None:
"""Test item descriptor in container from XML."""
didl_string = """
Album Container Title
object.container.album
-
Audio Item Title
object.item.audioItem
English
url
Text
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
container = items[0]
assert container is not None
assert isinstance(container, didl_lite.Container)
item = container[0]
assert item is not None
descriptor = item.descriptors[0]
assert descriptor is not None
assert descriptor.xml_el is not None
assert descriptor.id == "1"
assert descriptor.name_space == "ns"
assert descriptor.type == "type"
assert descriptor.text == "Text"
def test_descriptor_to_xml(self) -> None:
"""Test descriptor to XML."""
descriptor = didl_lite.Descriptor(
id="1", name_space="ns", type="type", text="Text"
)
item = didl_lite.AudioItem(
id="0",
parent_id="0",
title="Audio Item Title",
restricted="1",
language="English",
descriptors=[descriptor],
)
didl_string = didl_lite.to_xml_string(item).decode("utf-8")
didl_el = ET.fromstring(didl_string)
item_el = didl_el.find("./didl_lite:item", NAMESPACES)
assert item_el is not None
descriptor_el = item_el.find("./didl_lite:desc", NAMESPACES)
assert descriptor_el is not None
assert len(descriptor_el.attrib) == 3
assert descriptor_el.attrib["id"] == "1"
assert descriptor_el.attrib["nameSpace"] == "ns"
assert descriptor_el.attrib["type"] == "type"
assert descriptor_el.text == "Text"
descriptor = didl_lite.Descriptor(id="2", name_space="ns2")
descriptor_el = descriptor.to_xml()
assert descriptor_el is not None
assert len(descriptor_el.attrib) == 2
assert descriptor_el.attrib["id"] == "2"
assert descriptor_el.attrib["nameSpace"] == "ns2"
def test_descriptor_repr(self) -> None:
"""Test descriptor's repr can convert back to an equivalent descriptorb."""
# pylint: disable=import-outside-toplevel
from didl_lite.didl_lite import Descriptor
descriptor = Descriptor(id="1", name_space="ns", type="type", text="Text")
descriptor_repr = repr(descriptor)
descriptor_remade = eval(descriptor_repr) # pylint: disable=eval-used
assert ET.tostring(descriptor.to_xml()) == ET.tostring(
descriptor_remade.to_xml()
)
def test_item_order(self) -> None:
"""Test item ordering."""
didl_string = """
-
Audio Item Title 1
object.item.audioItem
English
Album Container Title
object.container.album
-
Audio Item Title 1
object.item.audioItem
English
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 3
assert isinstance(items[0], didl_lite.AudioItem)
assert isinstance(items[1], didl_lite.Album)
assert isinstance(items[2], didl_lite.AudioItem)
def test_item_property_attribute_from_xml(self) -> None:
"""Test item property from XML attribute."""
didl_string = """
-
Video Item Title
object.item.videoItem
Action
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
item = items[0]
assert item is not None
assert getattr(item, "genre") == "Action"
assert getattr(item, "genre_id") == "genreId"
def test_item_property_attribute_to_xml(self) -> None:
"""Test item property to XML."""
item = didl_lite.VideoItem(
id="0",
parent_id="0",
title="Video Item Title",
restricted="1",
genre="Action",
genre_id="genreId",
)
didl_string = didl_lite.to_xml_string(item).decode("utf-8")
didl_el = ET.fromstring(didl_string)
item_el = didl_el.find("./didl_lite:item", NAMESPACES)
assert item_el is not None
genre_el = item_el.find("./upnp:genre", NAMESPACES)
assert genre_el is not None
assert genre_el.text == "Action"
assert genre_el.attrib["id"] == "genreId"
def test_item_missing_id(self) -> None:
"""Test item missing ID from XML."""
didl_string = """
-
Video Item Title
object.item
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
def test_item_set_attributes(self) -> None:
"""Test item attribute from XML."""
didl_string = """
-
Video Item Title
object.item.videoItem
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
item = items[0]
assert getattr(item, "title") == "Video Item Title"
assert hasattr(item, "rating")
assert getattr(item, "rating") is None
assert isinstance(item, didl_lite.VideoItem)
assert len(item.res) == 0
assert item.res == item.resources
def test_extra_properties(self) -> None:
"""Test extra item properties from XML."""
didl_string = """
-
Video Item Title
object.item.videoItem
extra_property
"""
items = didl_lite.from_xml_string(didl_string)
assert len(items) == 1
item = items[0]
assert hasattr(item, "album_art_uri")
assert getattr(item, "album_art_uri") == "extra_property"
assert getattr(item, "albumArtURI") == "extra_property"
def test_default_properties_set(self) -> None:
"""Test defaults for item properties."""
item = didl_lite.VideoItem(
id="0", parent_id="0", title="Video Item Title", restricted="1"
)
assert hasattr(item, "genre_type") # property is set
def test_property_case(self) -> None:
"""Test item properties can be accessed using snake_case or camelCase."""
item = didl_lite.MusicTrack(
id="0",
parent_id="0",
title="Audio Item Title",
restricted="1",
language="English",
originalTrackNumber="1",
storage_medium="HDD",
)
assert hasattr(item, "original_track_number")
assert hasattr(item, "originalTrackNumber")
assert item.original_track_number is item.originalTrackNumber
assert item.original_track_number == "1"
assert hasattr(item, "storage_medium")
assert hasattr(item, "storageMedium")
assert item.storage_medium is item.storageMedium
assert item.storage_medium == "HDD"
assert hasattr(item, "long_description")
assert hasattr(item, "longDescription")
assert item.long_description is item.longDescription
assert item.long_description is None
assert not hasattr(item, "otherItem")
assert not hasattr(item, "other_item")
item.storageMedium = "CD"
assert item.storage_medium is item.storageMedium
assert item.storage_medium == "CD"
item.long_description = "Long description"
assert item.long_description is item.longDescription
assert item.long_description == "Long description"
item.otherItem = "otherItem"
assert hasattr(item, "otherItem")
assert not hasattr(item, "other_item")
assert item.otherItem == "otherItem"
def test_item_improper_class_nesting(self) -> None:
"""
Test item from XML that has upnp_class element above item.
Cater for WiiM Pro and possibly other Linkplay devices that
emit upnp_class above the item element instead of inside it
"""
didl_string = """
object.item.audioItem.musicTrack
-
Music Track Title
Music Track Creator
Artist
Album
"""
items = didl_lite.from_xml_string(didl_string, strict=False)
assert len(items) == 1
item = items[0]
assert isinstance(item, didl_lite.MusicTrack)
python-didl-lite-1.4.1/tox.ini 0000664 0000000 0000000 00000002341 14711665223 0016225 0 ustar 00root root 0000000 0000000 [tox]
envlist = py38, py39, py310, py311, py312, flake8, pylint, codespell, typing, black
[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311
3.12: py312, flake8, pylint, codespell, typing, black
3.13: py313
[testenv]
commands = py.test --cov=didl_lite --cov-report=term --cov-report=xml:coverage-{env_name}.xml {posargs}
ignore_errors = True
deps =
pytest == 7.4.3
pytest-asyncio ~= 0.23.2
pytest-cov ~= 4.1.0
coverage ~= 7.3.3
asyncmock ~= 0.4.2
[testenv:flake8]
basepython = python3
ignore_errors = True
deps =
flake8 ~= 6.1.0
flake8-docstrings == 1.7.0
flake8-noqa == 1.3.2
pydocstyle == 6.3.0
commands = flake8 didl_lite tests
[testenv:pylint]
basepython = python3
ignore_errors = True
deps =
pylint ~= 3.0.3
pytest == 7.4.3
commands = pylint didl_lite tests
[testenv:codespell]
basepython = python3
ignore_errors = True
deps =
codespell ~= 2.2.6
commands = codespell didl_lite tests
[testenv:typing]
basepython = python3
ignore_errors = True
deps =
mypy ~= 1.7.1
pytest == 7.4.3
commands = mypy --ignore-missing-imports didl_lite tests
[testenv:black]
basepython = python3
deps =
black >= 23.12.0
commands = black --diff didl_lite tests