python-didl-lite 1.4.0__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-didl-lite
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: DIDL-Lite (Digital Item Declaration Language) tools for Python
5
- Home-page: https://github.com/StevenLooman/python-didl-lite
6
- Author: Steven Looman
7
- Author-email: steven.looman@gmail.com
8
- License: UNKNOWN
9
- Platform: UNKNOWN
5
+ Author-email: Steven Looman <steven.looman@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: GitHub: repo, https://github.com/StevenLooman/python-didl-lite
8
+ Keywords: DIDL,DIDL-lite
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: Apache Software License
13
11
  Classifier: Programming Language :: Python
14
- Classifier: Programming Language :: Python :: 3.8
15
- Classifier: Programming Language :: Python :: 3.9
16
12
  Classifier: Programming Language :: Python :: 3.10
17
13
  Classifier: Programming Language :: Python :: 3.11
18
14
  Classifier: Programming Language :: Python :: 3.12
19
- Requires-Python: >=3.8
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.10
20
18
  Description-Content-Type: text/x-rst
21
19
  License-File: LICENSE.md
20
+ Requires-Dist: defusedxml>=0.6.0
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest==9.0.2; extra == "test"
23
+ Dynamic: license-file
22
24
 
23
25
  DIDL-Lite (Digital Item Declaration Language) tools for Python
24
26
  ==============================================================
@@ -44,3 +46,14 @@ DIDL-Lite resources and documents:
44
46
  * `mpeg21-didl <http://xml.coverpages.org/mpeg21-didl.html>`_
45
47
 
46
48
 
49
+ Releasing
50
+ ---------
51
+
52
+ Steps for releasing:
53
+
54
+ - Switch to development: ``git checkout master``
55
+ - Do a pull: ``git pull``
56
+ - Run towncrier: ``towncrier build --version <version>``
57
+ - Commit towncrier results: ``git commit -m "Towncrier"``
58
+ - Run bump2version (note that this creates a new commit + tag): ``bump2version --tag major/minor/patch``
59
+ - Push to github: ``git push && git push --tags``
@@ -20,3 +20,16 @@ DIDL-Lite resources and documents:
20
20
  * `UPnP-av-ContentDirectory-v2-Service <http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf>`_
21
21
  * `didl-lite-v2.xsd <http://www.upnp.org/schemas/av/didl-lite-v2.xsd>`_
22
22
  * `mpeg21-didl <http://xml.coverpages.org/mpeg21-didl.html>`_
23
+
24
+
25
+ Releasing
26
+ ---------
27
+
28
+ Steps for releasing:
29
+
30
+ - Switch to development: ``git checkout master``
31
+ - Do a pull: ``git pull``
32
+ - Run towncrier: ``towncrier build --version <version>``
33
+ - Commit towncrier results: ``git commit -m "Towncrier"``
34
+ - Run bump2version (note that this creates a new commit + tag): ``bump2version --tag major/minor/patch``
35
+ - Push to github: ``git push && git push --tags``
@@ -2,3 +2,5 @@
2
2
  """DIDL-Lite (Digital Item Declaration Language) tools for Python."""
3
3
 
4
4
  from didl_lite import didl_lite # noqa: F401
5
+
6
+ __version__ = "1.5.0"
@@ -2,6 +2,7 @@
2
2
  """DIDL-Lite (Digital Item Declaration Language) tools for Python."""
3
3
  # pylint: disable=too-many-lines
4
4
 
5
+ import re
5
6
  from typing import (
6
7
  Any,
7
8
  Dict,
@@ -86,7 +87,7 @@ class DidlObject:
86
87
  **properties: Any,
87
88
  ) -> None:
88
89
  """Initialize."""
89
- # pylint: disable=invalid-name,redefined-builtin,too-many-arguments
90
+ # pylint: disable=invalid-name,redefined-builtin,too-many-arguments,too-many-positional-arguments
90
91
  properties["id"] = id
91
92
  properties["parent_id"] = parent_id
92
93
  properties["class"] = self.upnp_class
@@ -627,7 +628,7 @@ class Container(DidlObject, list):
627
628
  **properties: Any,
628
629
  ) -> None:
629
630
  """Initialize."""
630
- # pylint: disable=redefined-builtin,too-many-arguments
631
+ # pylint: disable=redefined-builtin,too-many-arguments,too-many-positional-arguments
631
632
  super().__init__(id, parent_id, descriptors, xml_el, strict, **properties)
632
633
  self.extend(children)
633
634
 
@@ -929,7 +930,7 @@ class Resource:
929
930
  xml_el: Optional[ET.Element] = None,
930
931
  ) -> None:
931
932
  """Initialize."""
932
- # pylint: disable=too-many-arguments
933
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
933
934
  self.uri = uri
934
935
  self.protocol_info = protocol_info
935
936
  self.import_uri = import_uri
@@ -1007,7 +1008,7 @@ class Descriptor:
1007
1008
  xml_el: Optional[ET.Element] = None,
1008
1009
  ) -> None:
1009
1010
  """Initialize."""
1010
- # pylint: disable=invalid-name,redefined-builtin,too-many-arguments
1011
+ # pylint: disable=invalid-name,redefined-builtin,too-many-arguments,too-many-positional-arguments
1011
1012
  self.id = id
1012
1013
  self.name_space = name_space
1013
1014
  self.type = type
@@ -1073,7 +1074,27 @@ def to_xml_string(*objects: DidlObject) -> bytes:
1073
1074
  def from_xml_string(
1074
1075
  xml_string: str, strict: bool = True
1075
1076
  ) -> List[Union[DidlObject, Descriptor]]:
1076
- """Convert XML string to DIDL Objects."""
1077
+ """Parse DIDL-Lite XML string."""
1078
+ if not strict:
1079
+ # Find all prefixes used in tags, e.g., <prefix:tag ...>
1080
+ used_prefixes = set(re.findall(r"<([a-zA-Z0-9]+):", xml_string))
1081
+
1082
+ # Find all defined namespaces, e.g., xmlns:prefix=...
1083
+ defined_prefixes = set(re.findall(r"xmlns:([a-zA-Z0-9]+)=", xml_string))
1084
+
1085
+ # Identify prefixes used but not defined.
1086
+ missing_prefixes = (
1087
+ used_prefixes - defined_prefixes - {"DIDL-Lite", "dc", "upnp", "dlna"}
1088
+ )
1089
+
1090
+ # Remove the "if missing_prefixes:" line and just keep the for loop
1091
+ for prefix in missing_prefixes:
1092
+ dlna_ns = 'xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/"'
1093
+ if dlna_ns in xml_string:
1094
+ replacement = f'{dlna_ns} xmlns:{prefix}="http://tempuri.org/{prefix}/"'
1095
+ xml_string = xml_string.replace(dlna_ns, replacement)
1096
+
1097
+ # Proceed with parsing using the (potentially) patched xml_string
1077
1098
  xml_el = defusedxml.ElementTree.fromstring(xml_string)
1078
1099
  return from_xml_el(xml_el, strict)
1079
1100
 
@@ -1094,7 +1115,13 @@ def from_xml_el(
1094
1115
  # construct item
1095
1116
  upnp_class = child_el.find("./upnp:class", NAMESPACES)
1096
1117
  if upnp_class is None or not upnp_class.text:
1097
- continue
1118
+ if strict:
1119
+ continue
1120
+ # WiiM Pro and possibly other Linkplay devices emit
1121
+ # upnp_class above the item element instead of inside it
1122
+ upnp_class = xml_el.find("./upnp:class", NAMESPACES)
1123
+ if upnp_class is None or not upnp_class.text:
1124
+ continue
1098
1125
  didl_object_type = type_by_upnp_class(upnp_class.text, strict)
1099
1126
  if didl_object_type is None:
1100
1127
  if strict:
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools>=77.0"]
4
+
5
+ [project]
6
+ name = "python-didl-lite"
7
+ license = "Apache-2.0"
8
+ license-files = ["LICENSE.txt"]
9
+ description = "DIDL-Lite (Digital Item Declaration Language) tools for Python"
10
+ readme = "README.rst"
11
+ authors = [{ name = "Steven Looman", email = "steven.looman@gmail.com" }]
12
+ keywords = [
13
+ "DIDL",
14
+ "DIDL-lite"
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ ]
26
+ requires-python = ">=3.10"
27
+ dependencies = [
28
+ "defusedxml>=0.6.0",
29
+ ]
30
+ dynamic = ["version"]
31
+
32
+ [project.urls]
33
+ "GitHub: repo" = "https://github.com/StevenLooman/python-didl-lite"
34
+
35
+ [project.optional-dependencies]
36
+ test = [
37
+ "pytest==9.0.2",
38
+ ]
39
+
40
+ [tool.setuptools]
41
+ packages = ["didl_lite"]
42
+
43
+ [tool.setuptools.dynamic]
44
+ version = { attr = "didl_lite.__version__" }
45
+
46
+ [tool.setuptools.package-data]
47
+ didl_lite = ["py.typed"]
48
+
49
+ [tool.flake8]
50
+ max-line-length = 99
51
+
52
+ [tool.mypy]
53
+ check_untyped_defs = true
54
+ disallow_untyped_calls = true
55
+ disallow_untyped_defs = true
56
+ disallow_incomplete_defs = true
57
+ disallow_untyped_decorators = true
58
+ no_implicit_optional = true
59
+ warn_incomplete_stub = true
60
+ warn_redundant_casts = true
61
+ warn_return_any = true
62
+ warn_unused_configs = true
63
+ warn_unused_ignores = true
@@ -1,24 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-didl-lite
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: DIDL-Lite (Digital Item Declaration Language) tools for Python
5
- Home-page: https://github.com/StevenLooman/python-didl-lite
6
- Author: Steven Looman
7
- Author-email: steven.looman@gmail.com
8
- License: UNKNOWN
9
- Platform: UNKNOWN
5
+ Author-email: Steven Looman <steven.looman@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: GitHub: repo, https://github.com/StevenLooman/python-didl-lite
8
+ Keywords: DIDL,DIDL-lite
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: Apache Software License
13
11
  Classifier: Programming Language :: Python
14
- Classifier: Programming Language :: Python :: 3.8
15
- Classifier: Programming Language :: Python :: 3.9
16
12
  Classifier: Programming Language :: Python :: 3.10
17
13
  Classifier: Programming Language :: Python :: 3.11
18
14
  Classifier: Programming Language :: Python :: 3.12
19
- Requires-Python: >=3.8
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.10
20
18
  Description-Content-Type: text/x-rst
21
19
  License-File: LICENSE.md
20
+ Requires-Dist: defusedxml>=0.6.0
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest==9.0.2; extra == "test"
23
+ Dynamic: license-file
22
24
 
23
25
  DIDL-Lite (Digital Item Declaration Language) tools for Python
24
26
  ==============================================================
@@ -44,3 +46,14 @@ DIDL-Lite resources and documents:
44
46
  * `mpeg21-didl <http://xml.coverpages.org/mpeg21-didl.html>`_
45
47
 
46
48
 
49
+ Releasing
50
+ ---------
51
+
52
+ Steps for releasing:
53
+
54
+ - Switch to development: ``git checkout master``
55
+ - Do a pull: ``git pull``
56
+ - Run towncrier: ``towncrier build --version <version>``
57
+ - Commit towncrier results: ``git commit -m "Towncrier"``
58
+ - Run bump2version (note that this creates a new commit + tag): ``bump2version --tag major/minor/patch``
59
+ - Push to github: ``git push && git push --tags``
@@ -1,7 +1,7 @@
1
1
  LICENSE.md
2
2
  README.rst
3
+ pyproject.toml
3
4
  setup.cfg
4
- setup.py
5
5
  didl_lite/__init__.py
6
6
  didl_lite/didl_lite.py
7
7
  didl_lite/py.typed
@@ -0,0 +1,4 @@
1
+ defusedxml>=0.6.0
2
+
3
+ [test]
4
+ pytest==9.0.2
@@ -0,0 +1,51 @@
1
+ [bumpversion]
2
+ current_version = 1.5.0
3
+ commit = True
4
+ tag = False
5
+ tag_name = {new_version}
6
+
7
+ [bumpversion:file:didl_lite/__init__.py]
8
+ search = __version__ = "{current_version}"
9
+ replace = __version__ = "{new_version}"
10
+
11
+ [bdist_wheel]
12
+ python-tag = py3
13
+
14
+ [metadata]
15
+ license_file = LICENSE.md
16
+
17
+ [flake8]
18
+ exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
19
+ max-line-length = 119
20
+ max-complexity = 25
21
+ ignore =
22
+ E501,
23
+ W503,
24
+ E203,
25
+ D202,
26
+ W504
27
+ noqa-require-code = True
28
+
29
+ [mypy]
30
+ check_untyped_defs = true
31
+ disallow_untyped_calls = true
32
+ disallow_untyped_defs = true
33
+ disallow_incomplete_defs = true
34
+ disallow_untyped_decorators = true
35
+ no_implicit_optional = true
36
+ warn_incomplete_stub = true
37
+ warn_redundant_casts = true
38
+ warn_return_any = true
39
+ warn_unused_configs = true
40
+ warn_unused_ignores = true
41
+
42
+ [codespell]
43
+ ignore-words-list = wan
44
+
45
+ [coverage:run]
46
+ source = didl_lite
47
+
48
+ [egg_info]
49
+ tag_build =
50
+ tag_date = 0
51
+
@@ -654,3 +654,71 @@ class TestDidlLite:
654
654
  assert hasattr(item, "otherItem")
655
655
  assert not hasattr(item, "other_item")
656
656
  assert item.otherItem == "otherItem"
657
+
658
+ def test_item_improper_class_nesting(self) -> None:
659
+ """
660
+ Test item from XML that has upnp_class element above item.
661
+
662
+ Cater for WiiM Pro and possibly other Linkplay devices that
663
+ emit upnp_class above the item element instead of inside it
664
+ """
665
+ didl_string = """
666
+ <DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/"
667
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
668
+ xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
669
+ <upnp:class>object.item.audioItem.musicTrack</upnp:class>
670
+ <item id="0">
671
+ <res protocolInfo="protocol_info"></res>
672
+ <dc:title>Music Track Title</dc:title>
673
+ <dc:creator>Music Track Creator</dc:creator>
674
+ <upnp:artist>Artist</upnp:artist>
675
+ <upnp:album>Album</upnp:album>
676
+ </item>
677
+ </DIDL-Lite>"""
678
+ items = didl_lite.from_xml_string(didl_string, strict=False)
679
+ assert len(items) == 1
680
+
681
+ item = items[0]
682
+ assert isinstance(item, didl_lite.MusicTrack)
683
+
684
+ def test_from_xml_string_unbound_prefix(self) -> None:
685
+ """Test from_xml_string with unbound namespace prefix."""
686
+ # The key fix is adding: xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
687
+ broken_xml = (
688
+ '<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" '
689
+ 'xmlns:dc="http://purl.org/dc/elements/1.1/" '
690
+ 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" '
691
+ 'xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">'
692
+ '<item id="1" parentID="0" restricted="1">'
693
+ "<dc:title>Test Title</dc:title>"
694
+ "<song:subTitle>Test Subtitle</song:subTitle>"
695
+ "<upnp:class>object.item.audioItem.musicTrack</upnp:class>"
696
+ "</item>"
697
+ "</DIDL-Lite>"
698
+ )
699
+
700
+ # This call should not throw ParseError when strict=False.
701
+ objs = didl_lite.from_xml_string(broken_xml, strict=False)
702
+
703
+ assert len(objs) == 1
704
+ assert objs[0].title == "Test Title"
705
+ # Check that the temporary namespace is correctly assigned.
706
+ assert "sub_title" in objs[0].__dict__
707
+ assert objs[0].sub_title == "Test Subtitle"
708
+ assert isinstance(objs[0], didl_lite.MusicTrack)
709
+
710
+ def test_music_track_artist_and_genre(self) -> None:
711
+ """Test MusicTrack artist and genre properties."""
712
+ track = didl_lite.MusicTrack(
713
+ id="1",
714
+ parent_id="0",
715
+ title="Test",
716
+ restricted="0",
717
+ artist="My Artist",
718
+ genre="My Genre",
719
+ )
720
+
721
+ xml = didl_lite.to_xml_string(track)
722
+
723
+ assert b"<upnp:artist>My Artist</upnp:artist>" in xml
724
+ assert b"<upnp:genre>My Genre</upnp:genre>" in xml
@@ -1 +0,0 @@
1
- defusedxml>=0.6.0
@@ -1,26 +0,0 @@
1
- [bdist_wheel]
2
- python-tag = py3
3
-
4
- [metadata]
5
- license_file = LICENSE.md
6
-
7
- [flake8]
8
- max-line-length = 99
9
-
10
- [mypy]
11
- check_untyped_defs = true
12
- disallow_untyped_calls = true
13
- disallow_untyped_defs = true
14
- disallow_incomplete_defs = true
15
- disallow_untyped_decorators = true
16
- no_implicit_optional = true
17
- warn_incomplete_stub = true
18
- warn_redundant_casts = true
19
- warn_return_any = true
20
- warn_unused_configs = true
21
- warn_unused_ignores = true
22
-
23
- [egg_info]
24
- tag_build =
25
- tag_date = 0
26
-
@@ -1,51 +0,0 @@
1
- """Setup."""
2
-
3
- import os.path
4
-
5
- from setuptools import setup
6
-
7
- here = os.path.abspath(os.path.dirname(__file__))
8
- with open(os.path.join(here, "README.rst"), encoding="utf-8") as f:
9
- LONG_DESCRIPTION = f.read()
10
- with open(os.path.join(here, "README.rst"), encoding="utf-8") as f:
11
- DESCRIPTION = f.readline().strip()
12
-
13
-
14
- INSTALL_REQUIRES = [
15
- "defusedxml>=0.6.0",
16
- ]
17
-
18
-
19
- TEST_REQUIRES = [
20
- "pytest~=7.4.3",
21
- ]
22
-
23
-
24
- setup(
25
- name="python-didl-lite",
26
- version="1.4.0",
27
- description=DESCRIPTION,
28
- long_description=LONG_DESCRIPTION,
29
- long_description_content_type="text/x-rst",
30
- url="https://github.com/StevenLooman/python-didl-lite",
31
- author="Steven Looman",
32
- author_email="steven.looman@gmail.com",
33
- classifiers=[
34
- "Development Status :: 5 - Production/Stable",
35
- "Intended Audience :: Developers",
36
- "License :: OSI Approved :: Apache Software License",
37
- "Programming Language :: Python",
38
- "Programming Language :: Python :: 3.8",
39
- "Programming Language :: Python :: 3.9",
40
- "Programming Language :: Python :: 3.10",
41
- "Programming Language :: Python :: 3.11",
42
- "Programming Language :: Python :: 3.12",
43
- ],
44
- packages=["didl_lite"],
45
- package_data={
46
- "didl_lite": ["py.typed"],
47
- },
48
- python_requires=">=3.8",
49
- install_requires=INSTALL_REQUIRES,
50
- tests_require=TEST_REQUIRES,
51
- )