python-didl-lite 1.3.2__tar.gz → 1.4.1__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.
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-didl-lite
3
+ Version: 1.4.1
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
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/x-rst
20
+ License-File: LICENSE.md
21
+ Requires-Dist: defusedxml>=0.6.0
22
+
23
+ DIDL-Lite (Digital Item Declaration Language) tools for Python
24
+ ==============================================================
25
+
26
+ DIDL-Lite tools for Python to read and write DIDL-Lite-xml.
27
+
28
+ Usage
29
+ -----
30
+
31
+ See ``tests/`` for examples on how to use this.
32
+
33
+ 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.
34
+
35
+
36
+ Resources / documents
37
+ ---------------------
38
+
39
+ DIDL-Lite resources and documents:
40
+
41
+ * `UPnP-av-ContentDirectory-v1-Service <http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf>`_
42
+ * `UPnP-av-ContentDirectory-v2-Service <http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf>`_
43
+ * `didl-lite-v2.xsd <http://www.upnp.org/schemas/av/didl-lite-v2.xsd>`_
44
+ * `mpeg21-didl <http://xml.coverpages.org/mpeg21-didl.html>`_
@@ -16,7 +16,7 @@ Resources / documents
16
16
 
17
17
  DIDL-Lite resources and documents:
18
18
 
19
- * [UPnP-av-ContentDirectory-v1-Service](http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf)
20
- * [UPnP-av-ContentDirectory-v2-Service](http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf)
21
- * [didl-lite-v2.xsd](http://www.upnp.org/schemas/av/didl-lite-v2.xsd)
22
- * [mpeg21-didl](http://xml.coverpages.org/mpeg21-didl.html)
19
+ * `UPnP-av-ContentDirectory-v1-Service <http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf>`_
20
+ * `UPnP-av-ContentDirectory-v2-Service <http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf>`_
21
+ * `didl-lite-v2.xsd <http://www.upnp.org/schemas/av/didl-lite-v2.xsd>`_
22
+ * `mpeg21-didl <http://xml.coverpages.org/mpeg21-didl.html>`_
@@ -28,10 +28,10 @@ from .utils import (
28
28
  to_camel_case,
29
29
  )
30
30
 
31
- TDO = TypeVar("TDO", bound="DidlObject")
32
- TC = TypeVar("TC", bound="Container")
33
- TD = TypeVar("TD", bound="Descriptor")
34
- TR = TypeVar("TR", bound="Resource")
31
+ TDO = TypeVar("TDO", bound="DidlObject") # pylint: disable=invalid-name
32
+ TC = TypeVar("TC", bound="Container") # pylint: disable=invalid-name
33
+ TD = TypeVar("TD", bound="Descriptor") # pylint: disable=invalid-name
34
+ TR = TypeVar("TR", bound="Resource") # pylint: disable=invalid-name
35
35
 
36
36
 
37
37
  class DidlLiteException(Exception):
@@ -39,6 +39,12 @@ class DidlLiteException(Exception):
39
39
 
40
40
 
41
41
  # region: DidlObjects
42
+
43
+ # upnp_class to python type mapping
44
+ _upnp_class_map: Dict[str, Type["DidlObject"]] = {}
45
+ _upnp_class_map_lowercase: Dict[str, Type["DidlObject"]] = {}
46
+
47
+
42
48
  class DidlObject:
43
49
  """DIDL Object."""
44
50
 
@@ -61,6 +67,15 @@ class DidlObject:
61
67
  xml_el: Optional[ET.Element]
62
68
  descriptors: Sequence["Descriptor"]
63
69
 
70
+ @classmethod
71
+ def __init_subclass__(cls: Type["DidlObject"], **kwargs: Any) -> None:
72
+ """Create mapping of upnp_class to Python type for fast lookup."""
73
+ super().__init_subclass__(**kwargs)
74
+ assert cls.upnp_class not in _upnp_class_map
75
+ assert cls.upnp_class.lower() not in _upnp_class_map_lowercase
76
+ _upnp_class_map[cls.upnp_class] = cls
77
+ _upnp_class_map_lowercase[cls.upnp_class.lower()] = cls
78
+
64
79
  def __init__(
65
80
  self,
66
81
  id: str = "",
@@ -85,9 +100,6 @@ class DidlObject:
85
100
  self.xml_el = xml_el
86
101
  self.descriptors = descriptors if descriptors else []
87
102
 
88
- def _clean_property_names(self, properties: Dict[str, Any]) -> Dict[str, Any]:
89
- """Turn all property names into lower_camel_case."""
90
-
91
103
  def _ensure_required_properties(
92
104
  self, strict: bool, properties: Mapping[str, Any]
93
105
  ) -> None:
@@ -1082,9 +1094,17 @@ def from_xml_el(
1082
1094
  # construct item
1083
1095
  upnp_class = child_el.find("./upnp:class", NAMESPACES)
1084
1096
  if upnp_class is None or not upnp_class.text:
1085
- continue
1086
- didl_object_type = type_by_upnp_class(upnp_class.text)
1097
+ if strict:
1098
+ continue
1099
+ # WiiM Pro and possibly other Linkplay devices emit
1100
+ # upnp_class above the item element instead of inside it
1101
+ upnp_class = xml_el.find("./upnp:class", NAMESPACES)
1102
+ if upnp_class is None or not upnp_class.text:
1103
+ continue
1104
+ didl_object_type = type_by_upnp_class(upnp_class.text, strict)
1087
1105
  if didl_object_type is None:
1106
+ if strict:
1107
+ raise DidlLiteException(f"upnp:class {upnp_class.text} is unknown")
1088
1108
  continue
1089
1109
  didl_object = didl_object_type.from_xml(child_el, strict)
1090
1110
  didl_objects.append(didl_object)
@@ -1098,13 +1118,14 @@ def from_xml_el(
1098
1118
 
1099
1119
 
1100
1120
  # upnp_class to python type mapping
1101
- def type_by_upnp_class(upnp_class: str) -> Optional[Type[DidlObject]]:
1102
- """Get DidlObject-type by upnp_class."""
1103
- queue = DidlObject.__subclasses__()
1104
- while queue:
1105
- type_ = queue.pop()
1106
- queue.extend(type_.__subclasses__())
1107
-
1108
- if type_.upnp_class == upnp_class:
1109
- return type_
1110
- return None
1121
+ def type_by_upnp_class(
1122
+ upnp_class: str, strict: bool = True
1123
+ ) -> Optional[Type[DidlObject]]:
1124
+ """Get DidlObject-type by upnp_class.
1125
+
1126
+ When strict is False, the upnp_class lookup will be done ignoring string
1127
+ case.
1128
+ """
1129
+ if strict:
1130
+ return _upnp_class_map.get(upnp_class)
1131
+ return _upnp_class_map_lowercase.get(upnp_class.lower())
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-didl-lite
3
+ Version: 1.4.1
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
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/x-rst
20
+ License-File: LICENSE.md
21
+ Requires-Dist: defusedxml>=0.6.0
22
+
23
+ DIDL-Lite (Digital Item Declaration Language) tools for Python
24
+ ==============================================================
25
+
26
+ DIDL-Lite tools for Python to read and write DIDL-Lite-xml.
27
+
28
+ Usage
29
+ -----
30
+
31
+ See ``tests/`` for examples on how to use this.
32
+
33
+ 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.
34
+
35
+
36
+ Resources / documents
37
+ ---------------------
38
+
39
+ DIDL-Lite resources and documents:
40
+
41
+ * `UPnP-av-ContentDirectory-v1-Service <http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf>`_
42
+ * `UPnP-av-ContentDirectory-v2-Service <http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf>`_
43
+ * `didl-lite-v2.xsd <http://www.upnp.org/schemas/av/didl-lite-v2.xsd>`_
44
+ * `mpeg21-didl <http://xml.coverpages.org/mpeg21-didl.html>`_
@@ -10,4 +10,5 @@ python_didl_lite.egg-info/PKG-INFO
10
10
  python_didl_lite.egg-info/SOURCES.txt
11
11
  python_didl_lite.egg-info/dependency_links.txt
12
12
  python_didl_lite.egg-info/requires.txt
13
- python_didl_lite.egg-info/top_level.txt
13
+ python_didl_lite.egg-info/top_level.txt
14
+ tests/test_didl_lite.py
@@ -17,13 +17,13 @@ INSTALL_REQUIRES = [
17
17
 
18
18
 
19
19
  TEST_REQUIRES = [
20
- "pytest~=6.1.2",
20
+ "pytest~=7.4.3",
21
21
  ]
22
22
 
23
23
 
24
24
  setup(
25
25
  name="python-didl-lite",
26
- version="1.3.2",
26
+ version="1.4.1",
27
27
  description=DESCRIPTION,
28
28
  long_description=LONG_DESCRIPTION,
29
29
  long_description_content_type="text/x-rst",
@@ -35,15 +35,18 @@ setup(
35
35
  "Intended Audience :: Developers",
36
36
  "License :: OSI Approved :: Apache Software License",
37
37
  "Programming Language :: Python",
38
- "Programming Language :: Python :: 3.6",
39
- "Programming Language :: Python :: 3.7",
40
38
  "Programming Language :: Python :: 3.8",
41
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
+ "Programming Language :: Python :: 3.13",
42
44
  ],
43
45
  packages=["didl_lite"],
44
46
  package_data={
45
47
  "didl_lite": ["py.typed"],
46
48
  },
49
+ python_requires=">=3.8",
47
50
  install_requires=INSTALL_REQUIRES,
48
51
  tests_require=TEST_REQUIRES,
49
52
  )
@@ -0,0 +1,682 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Unit tests for didl_lite."""
3
+
4
+ import pytest
5
+ from defusedxml import ElementTree as ET
6
+
7
+ from didl_lite import didl_lite
8
+
9
+ NAMESPACES = {
10
+ "didl_lite": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
11
+ "dc": "http://purl.org/dc/elements/1.1/",
12
+ "upnp": "urn:schemas-upnp-org:metadata-1-0/upnp/",
13
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
14
+ }
15
+
16
+
17
+ class TestDidlLite:
18
+ """Tests for didl_lite."""
19
+
20
+ # pylint: disable=too-many-public-methods
21
+
22
+ def test_item_from_xml(self) -> None:
23
+ """Test item from XML."""
24
+ didl_string = """
25
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
26
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
27
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
28
+ xmlns:sec="http://www.sec.co.kr/">
29
+ <item id="0" parentID="0" restricted="1">
30
+ <dc:title>Audio Item Title</dc:title>
31
+ <upnp:class>object.item.audioItem</upnp:class>
32
+ <dc:language>English</dc:language>
33
+ <res protocolInfo="protocol_info">url</res>
34
+ <upnp:longDescription>Long description</upnp:longDescription>
35
+ </item>
36
+ </DIDL-Lite>"""
37
+ items = didl_lite.from_xml_string(didl_string)
38
+ assert len(items) == 1
39
+
40
+ item = items[0]
41
+ assert item.xml_el is not None
42
+ assert getattr(item, "title") == "Audio Item Title"
43
+ assert getattr(item, "upnp_class") == "object.item.audioItem"
44
+ assert getattr(item, "language") == "English"
45
+ assert getattr(item, "longDescription") == "Long description"
46
+ assert getattr(item, "long_description") == "Long description"
47
+ assert isinstance(item, didl_lite.AudioItem)
48
+ assert not hasattr(item, "non_existing")
49
+
50
+ resources = item.res
51
+ assert len(resources) == 1
52
+ resource = resources[0]
53
+ assert resource.xml_el is not None
54
+ assert resource.protocol_info == "protocol_info"
55
+ assert resource.uri == "url"
56
+ assert not hasattr(item, "non_existing")
57
+ assert item.res == item.resources
58
+
59
+ def test_item_bad_class(self) -> None:
60
+ """Test item from XML that has a badly-cased upnp:class."""
61
+ didl_string = """
62
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
63
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
64
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
65
+ xmlns:sec="http://www.sec.co.kr/">
66
+ <item id="0" parentID="0" restricted="1">
67
+ <dc:title>Audio Item Title</dc:title>
68
+ <upnp:class>Object.Item.AudioItem</upnp:class>
69
+ <dc:language>English</dc:language>
70
+ <res protocolInfo="protocol_info">url</res>
71
+ <upnp:longDescription>Long description</upnp:longDescription>
72
+ </item>
73
+ </DIDL-Lite>"""
74
+ with pytest.raises(
75
+ didl_lite.DidlLiteException,
76
+ match="upnp:class Object.Item.AudioItem is unknown",
77
+ ):
78
+ didl_lite.from_xml_string(didl_string)
79
+
80
+ items = didl_lite.from_xml_string(didl_string, strict=False)
81
+ assert len(items) == 1
82
+
83
+ item = items[0]
84
+ assert isinstance(item, didl_lite.AudioItem)
85
+
86
+ def test_item_from_xml_not_strict(self) -> None:
87
+ """Test item from XML."""
88
+ didl_string = """
89
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
90
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
91
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
92
+ xmlns:sec="http://www.sec.co.kr/">
93
+ <item id="0" parentID="0" restricted="1">
94
+ <dc:title>Audio Item Title</dc:title>
95
+ <upnp:class>object.item.audioItem</upnp:class>
96
+ <dc:language>English</dc:language>
97
+ <res>url</res>
98
+ </item>
99
+ </DIDL-Lite>"""
100
+ items = didl_lite.from_xml_string(didl_string, strict=False)
101
+ assert len(items) == 1
102
+
103
+ item = items[0]
104
+ assert item.xml_el is not None
105
+ assert getattr(item, "title") == "Audio Item Title"
106
+ assert getattr(item, "upnp_class") == "object.item.audioItem"
107
+ assert getattr(item, "language") == "English"
108
+ assert isinstance(item, didl_lite.AudioItem)
109
+ assert not hasattr(item, "non_existing")
110
+
111
+ resources = item.res
112
+ assert len(resources) == 1
113
+ resource = resources[0]
114
+ assert resource.xml_el is not None
115
+ assert resource.protocol_info is None # This is now allowed with strict=False
116
+ assert resource.uri == "url"
117
+ assert not hasattr(item, "non_existing")
118
+ assert item.res == item.resources
119
+
120
+ def test_item_to_xml(self) -> None:
121
+ """Test item to XML."""
122
+ resource = didl_lite.Resource("url", "protocol_info")
123
+ items = [
124
+ didl_lite.AudioItem(
125
+ id="0",
126
+ parent_id="0",
127
+ title="Audio Item Title",
128
+ restricted="1",
129
+ resources=[resource],
130
+ language="English",
131
+ longDescription="Long description",
132
+ ),
133
+ ]
134
+ didl_string = didl_lite.to_xml_string(*items).decode("utf-8")
135
+
136
+ assert 'xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"' in didl_string
137
+ assert 'xmlns:dc="http://purl.org/dc/elements/1.1/"' in didl_string
138
+ assert 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"' in didl_string
139
+ assert 'xmlns:sec="http://www.sec.co.kr/"' in didl_string
140
+ assert 'xmlns:ns1="urn:schemas-upnp-org:metadata-1-0/upnp/"' not in didl_string
141
+
142
+ didl_el = ET.fromstring(didl_string)
143
+
144
+ item_el = didl_el.find("./didl_lite:item", NAMESPACES)
145
+ assert item_el is not None
146
+ assert item_el.attrib["id"] == "0"
147
+ assert item_el.attrib["parentID"] == "0"
148
+ assert item_el.attrib["restricted"] == "1"
149
+
150
+ title_el = item_el.find("./dc:title", NAMESPACES)
151
+ assert title_el is not None
152
+ assert title_el.text == "Audio Item Title"
153
+
154
+ class_el = item_el.find("./upnp:class", NAMESPACES)
155
+ assert class_el is not None
156
+ assert class_el.text == "object.item.audioItem"
157
+
158
+ language_el = item_el.find("./dc:language", NAMESPACES)
159
+ assert language_el is not None
160
+ assert language_el.text == "English"
161
+
162
+ long_description_el = item_el.find("./upnp:longDescription", NAMESPACES)
163
+ assert long_description_el is not None
164
+ assert long_description_el.text == "Long description"
165
+
166
+ res_el = item_el.find("./didl_lite:res", NAMESPACES)
167
+ assert res_el is not None
168
+ assert res_el.attrib["protocolInfo"] == "protocol_info"
169
+ assert res_el.text == "url"
170
+
171
+ def test_item_repr(self) -> None:
172
+ """Test item's repr can convert back to an equivalent item."""
173
+ # pylint: disable=import-outside-toplevel
174
+ # repr method doesn't know how package was imported, so only uses class names
175
+ from didl_lite.didl_lite import AudioItem, Resource
176
+
177
+ item = AudioItem(
178
+ id="0",
179
+ parent_id="0",
180
+ title="Audio Item Title",
181
+ restricted="1",
182
+ res=[
183
+ Resource("url", "protocol_info"),
184
+ Resource("url2", "protocol_info2"),
185
+ ],
186
+ language="English",
187
+ )
188
+
189
+ item_repr = repr(item)
190
+ item_remade = eval(item_repr) # pylint: disable=eval-used
191
+ assert ET.tostring(item.to_xml()) == ET.tostring(item_remade.to_xml())
192
+
193
+ def test_container_from_xml(self) -> None:
194
+ """Test container from XML."""
195
+ didl_string = """
196
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
197
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
198
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
199
+ xmlns:sec="http://www.sec.co.kr/">
200
+ <container id="0" parentID="0" restricted="1">
201
+ <dc:title>Album Container Title</dc:title>
202
+ <upnp:class>object.container.album</upnp:class>
203
+
204
+ <item id="1" parentID="0" restricted="1">
205
+ <dc:title>Audio Item Title</dc:title>
206
+ <upnp:class>object.item.audioItem</upnp:class>
207
+ <dc:language>English</dc:language>
208
+ <res protocolInfo="protocol_info">url</res>
209
+ </item>
210
+ </container>
211
+ </DIDL-Lite>"""
212
+ items = didl_lite.from_xml_string(didl_string)
213
+ assert len(items) == 1
214
+
215
+ container = items[0]
216
+ assert container.xml_el is not None
217
+ assert isinstance(container, didl_lite.Container)
218
+ assert getattr(container, "title") == "Album Container Title"
219
+ assert getattr(container, "upnp_class") == "object.container.album"
220
+
221
+ item = container[0]
222
+ assert item.xml_el is not None
223
+ assert item.title == "Audio Item Title"
224
+ assert item.upnp_class == "object.item.audioItem"
225
+ assert item.language == "English"
226
+
227
+ resources = item.res
228
+ assert len(resources) == 1
229
+ resource = resources[0]
230
+ assert resource.xml_el is not None
231
+ assert resource.protocol_info == "protocol_info"
232
+ assert resource.uri == "url"
233
+ assert item.res == item.resources
234
+
235
+ def test_container_to_xml(self) -> None:
236
+ """Test container to XML."""
237
+ container = didl_lite.Album(
238
+ id="0", parent_id="0", title="Audio Item Title", restricted="1"
239
+ )
240
+ resource = didl_lite.Resource("url", "protocol_info")
241
+ item = didl_lite.AudioItem(
242
+ id="0",
243
+ parent_id="0",
244
+ title="Audio Item Title",
245
+ restricted="1",
246
+ resources=[resource],
247
+ language="English",
248
+ )
249
+ container.append(item)
250
+
251
+ didl_string = didl_lite.to_xml_string(container).decode("utf-8")
252
+ didl_el = ET.fromstring(didl_string)
253
+
254
+ container_el = didl_el.find("./didl_lite:container", NAMESPACES)
255
+ assert container_el is not None
256
+ assert container_el.attrib["id"] == "0"
257
+ assert container_el.attrib["parentID"] == "0"
258
+ assert container_el.attrib["restricted"] == "1"
259
+
260
+ item_el = container_el.find("./didl_lite:item", NAMESPACES)
261
+ assert item_el is not None
262
+ assert item_el.attrib["id"] == "0"
263
+ assert item_el.attrib["parentID"] == "0"
264
+ assert item_el.attrib["restricted"] == "1"
265
+
266
+ title_el = item_el.find("./dc:title", NAMESPACES)
267
+ assert title_el is not None
268
+ assert title_el.text == "Audio Item Title"
269
+
270
+ class_el = item_el.find("./upnp:class", NAMESPACES)
271
+ assert class_el is not None
272
+ assert class_el.text == "object.item.audioItem"
273
+
274
+ language_el = item_el.find("./dc:language", NAMESPACES)
275
+ assert language_el is not None
276
+ assert language_el.text == "English"
277
+
278
+ res_el = item_el.find("./didl_lite:res", NAMESPACES)
279
+ assert res_el is not None
280
+ assert res_el.attrib["protocolInfo"] == "protocol_info"
281
+ assert res_el.text == "url"
282
+
283
+ def test_container_repr(self) -> None:
284
+ """Test containers's repr can convert back to an equivalent container."""
285
+ # pylint: disable=import-outside-toplevel
286
+ from didl_lite.didl_lite import Album, AudioItem, Resource
287
+
288
+ container = Album(
289
+ id="0", parent_id="0", title="Audio Item Title", restricted="1"
290
+ )
291
+ resource = Resource("url", "protocol_info")
292
+ item = AudioItem(
293
+ id="0",
294
+ parent_id="0",
295
+ title="Audio Item Title",
296
+ restricted="1",
297
+ resources=[resource],
298
+ language="English",
299
+ )
300
+ container.append(item)
301
+
302
+ container_repr = repr(container)
303
+ container_remade = eval(container_repr) # pylint: disable=eval-used
304
+ assert ET.tostring(container.to_xml()) == ET.tostring(container_remade.to_xml())
305
+
306
+ def test_descriptor_from_xml_root(self) -> None:
307
+ """Test root descriptor from XML."""
308
+ didl_string = """
309
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
310
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
311
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
312
+ xmlns:sec="http://www.sec.co.kr/">
313
+ <desc id="1" nameSpace="ns" type="type">Text</desc>
314
+ </DIDL-Lite>"""
315
+
316
+ items = didl_lite.from_xml_string(didl_string)
317
+ assert len(items) == 1
318
+
319
+ descriptor = items[0]
320
+ assert descriptor is not None
321
+ assert descriptor.xml_el is not None
322
+ assert getattr(descriptor, "id") == "1"
323
+ assert getattr(descriptor, "name_space") == "ns"
324
+ assert getattr(descriptor, "type") == "type"
325
+ assert getattr(descriptor, "text") == "Text"
326
+
327
+ def test_descriptor_from_xml_item(self) -> None:
328
+ """Test item descriptor from XML."""
329
+ didl_string = """
330
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
331
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
332
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
333
+ xmlns:sec="http://www.sec.co.kr/">
334
+ <item id="1" parentID="0" restricted="1">
335
+ <dc:title>Audio Item Title</dc:title>
336
+ <upnp:class>object.item.audioItem</upnp:class>
337
+ <dc:language>English</dc:language>
338
+ <res protocolInfo="protocol_info">url</res>
339
+ <desc id="1" nameSpace="ns" type="type">Text</desc>
340
+ </item>
341
+ </DIDL-Lite>"""
342
+
343
+ items = didl_lite.from_xml_string(didl_string)
344
+ assert len(items) == 1
345
+
346
+ item = items[0]
347
+ assert item is not None
348
+ assert isinstance(item, didl_lite.AudioItem)
349
+
350
+ descriptor = item.descriptors[0]
351
+ assert descriptor is not None
352
+ assert descriptor.xml_el is not None
353
+ assert descriptor.id == "1"
354
+ assert descriptor.name_space == "ns"
355
+ assert descriptor.type == "type"
356
+ assert descriptor.text == "Text"
357
+
358
+ def test_descriptor_from_xml_container(self) -> None:
359
+ """Test container descriptor from XML."""
360
+ didl_string = """
361
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
362
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
363
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
364
+ xmlns:sec="http://www.sec.co.kr/">
365
+ <container id="0" parentID="0" restricted="1">
366
+ <dc:title>Album Container Title</dc:title>
367
+ <upnp:class>object.container.album</upnp:class>
368
+
369
+ <desc id="1" nameSpace="ns" type="type">Text</desc>
370
+ </container>
371
+ </DIDL-Lite>"""
372
+
373
+ items = didl_lite.from_xml_string(didl_string)
374
+ assert len(items) == 1
375
+
376
+ container = items[0]
377
+ assert container is not None
378
+ assert container.xml_el is not None
379
+ assert isinstance(container, didl_lite.Container)
380
+
381
+ descriptor = container.descriptors[0]
382
+ assert descriptor is not None
383
+ assert descriptor.xml_el is not None
384
+ assert descriptor.id == "1"
385
+ assert descriptor.name_space == "ns"
386
+ assert descriptor.type == "type"
387
+ assert descriptor.text == "Text"
388
+
389
+ def test_descriptor_from_xml_container_item(self) -> None:
390
+ """Test item descriptor in container from XML."""
391
+ didl_string = """
392
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
393
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
394
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
395
+ xmlns:sec="http://www.sec.co.kr/">
396
+ <container id="0" parentID="0" restricted="1">
397
+ <dc:title>Album Container Title</dc:title>
398
+ <upnp:class>object.container.album</upnp:class>
399
+
400
+ <item id="1" parentID="0" restricted="1">
401
+ <dc:title>Audio Item Title</dc:title>
402
+ <upnp:class>object.item.audioItem</upnp:class>
403
+ <dc:language>English</dc:language>
404
+ <res protocolInfo="protocol_info">url</res>
405
+ <desc id="1" nameSpace="ns" type="type">Text</desc>
406
+ </item>
407
+ </container>
408
+ </DIDL-Lite>"""
409
+
410
+ items = didl_lite.from_xml_string(didl_string)
411
+ assert len(items) == 1
412
+
413
+ container = items[0]
414
+ assert container is not None
415
+ assert isinstance(container, didl_lite.Container)
416
+
417
+ item = container[0]
418
+ assert item is not None
419
+
420
+ descriptor = item.descriptors[0]
421
+ assert descriptor is not None
422
+ assert descriptor.xml_el is not None
423
+ assert descriptor.id == "1"
424
+ assert descriptor.name_space == "ns"
425
+ assert descriptor.type == "type"
426
+ assert descriptor.text == "Text"
427
+
428
+ def test_descriptor_to_xml(self) -> None:
429
+ """Test descriptor to XML."""
430
+ descriptor = didl_lite.Descriptor(
431
+ id="1", name_space="ns", type="type", text="Text"
432
+ )
433
+ item = didl_lite.AudioItem(
434
+ id="0",
435
+ parent_id="0",
436
+ title="Audio Item Title",
437
+ restricted="1",
438
+ language="English",
439
+ descriptors=[descriptor],
440
+ )
441
+ didl_string = didl_lite.to_xml_string(item).decode("utf-8")
442
+ didl_el = ET.fromstring(didl_string)
443
+
444
+ item_el = didl_el.find("./didl_lite:item", NAMESPACES)
445
+ assert item_el is not None
446
+
447
+ descriptor_el = item_el.find("./didl_lite:desc", NAMESPACES)
448
+ assert descriptor_el is not None
449
+ assert len(descriptor_el.attrib) == 3
450
+ assert descriptor_el.attrib["id"] == "1"
451
+ assert descriptor_el.attrib["nameSpace"] == "ns"
452
+ assert descriptor_el.attrib["type"] == "type"
453
+ assert descriptor_el.text == "Text"
454
+
455
+ descriptor = didl_lite.Descriptor(id="2", name_space="ns2")
456
+ descriptor_el = descriptor.to_xml()
457
+ assert descriptor_el is not None
458
+ assert len(descriptor_el.attrib) == 2
459
+ assert descriptor_el.attrib["id"] == "2"
460
+ assert descriptor_el.attrib["nameSpace"] == "ns2"
461
+
462
+ def test_descriptor_repr(self) -> None:
463
+ """Test descriptor's repr can convert back to an equivalent descriptorb."""
464
+ # pylint: disable=import-outside-toplevel
465
+ from didl_lite.didl_lite import Descriptor
466
+
467
+ descriptor = Descriptor(id="1", name_space="ns", type="type", text="Text")
468
+
469
+ descriptor_repr = repr(descriptor)
470
+ descriptor_remade = eval(descriptor_repr) # pylint: disable=eval-used
471
+ assert ET.tostring(descriptor.to_xml()) == ET.tostring(
472
+ descriptor_remade.to_xml()
473
+ )
474
+
475
+ def test_item_order(self) -> None:
476
+ """Test item ordering."""
477
+ didl_string = """
478
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
479
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
480
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
481
+ xmlns:sec="http://www.sec.co.kr/">
482
+ <item id="0" parentID="0" restricted="1">
483
+ <dc:title>Audio Item Title 1</dc:title>
484
+ <upnp:class>object.item.audioItem</upnp:class>
485
+ <dc:language>English</dc:language>
486
+ </item>
487
+ <container id="1" parentID="0" restricted="1">
488
+ <dc:title>Album Container Title</dc:title>
489
+ <upnp:class>object.container.album</upnp:class>
490
+ </container>
491
+ <item id="2" parentID="0" restricted="1">
492
+ <dc:title>Audio Item Title 1</dc:title>
493
+ <upnp:class>object.item.audioItem</upnp:class>
494
+ <dc:language>English</dc:language>
495
+ </item>
496
+ </DIDL-Lite>"""
497
+
498
+ items = didl_lite.from_xml_string(didl_string)
499
+ assert len(items) == 3
500
+
501
+ assert isinstance(items[0], didl_lite.AudioItem)
502
+ assert isinstance(items[1], didl_lite.Album)
503
+ assert isinstance(items[2], didl_lite.AudioItem)
504
+
505
+ def test_item_property_attribute_from_xml(self) -> None:
506
+ """Test item property from XML attribute."""
507
+ didl_string = """
508
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
509
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
510
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
511
+ xmlns:sec="http://www.sec.co.kr/">
512
+ <item id="0" parentID="0" restricted="1">
513
+ <dc:title>Video Item Title</dc:title>
514
+ <upnp:class>object.item.videoItem</upnp:class>
515
+ <upnp:genre id="genreId">Action</upnp:genre>
516
+ </item>
517
+ </DIDL-Lite>"""
518
+
519
+ items = didl_lite.from_xml_string(didl_string)
520
+ assert len(items) == 1
521
+
522
+ item = items[0]
523
+ assert item is not None
524
+ assert getattr(item, "genre") == "Action"
525
+ assert getattr(item, "genre_id") == "genreId"
526
+
527
+ def test_item_property_attribute_to_xml(self) -> None:
528
+ """Test item property to XML."""
529
+ item = didl_lite.VideoItem(
530
+ id="0",
531
+ parent_id="0",
532
+ title="Video Item Title",
533
+ restricted="1",
534
+ genre="Action",
535
+ genre_id="genreId",
536
+ )
537
+ didl_string = didl_lite.to_xml_string(item).decode("utf-8")
538
+ didl_el = ET.fromstring(didl_string)
539
+
540
+ item_el = didl_el.find("./didl_lite:item", NAMESPACES)
541
+ assert item_el is not None
542
+
543
+ genre_el = item_el.find("./upnp:genre", NAMESPACES)
544
+ assert genre_el is not None
545
+ assert genre_el.text == "Action"
546
+ assert genre_el.attrib["id"] == "genreId"
547
+
548
+ def test_item_missing_id(self) -> None:
549
+ """Test item missing ID from XML."""
550
+ didl_string = """
551
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
552
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
553
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
554
+ xmlns:sec="http://www.sec.co.kr/">
555
+ <item restricted="1">
556
+ <dc:title>Video Item Title</dc:title>
557
+ <upnp:class>object.item</upnp:class>
558
+ </item>
559
+ </DIDL-Lite>"""
560
+
561
+ items = didl_lite.from_xml_string(didl_string)
562
+ assert len(items) == 1
563
+
564
+ def test_item_set_attributes(self) -> None:
565
+ """Test item attribute from XML."""
566
+ didl_string = """
567
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
568
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
569
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
570
+ xmlns:sec="http://www.sec.co.kr/">
571
+ <item restricted="1">
572
+ <dc:title>Video Item Title</dc:title>
573
+ <upnp:class>object.item.videoItem</upnp:class>
574
+ </item>
575
+ </DIDL-Lite>"""
576
+
577
+ items = didl_lite.from_xml_string(didl_string)
578
+ assert len(items) == 1
579
+
580
+ item = items[0]
581
+ assert getattr(item, "title") == "Video Item Title"
582
+ assert hasattr(item, "rating")
583
+ assert getattr(item, "rating") is None
584
+ assert isinstance(item, didl_lite.VideoItem)
585
+ assert len(item.res) == 0
586
+ assert item.res == item.resources
587
+
588
+ def test_extra_properties(self) -> None:
589
+ """Test extra item properties from XML."""
590
+ didl_string = """
591
+ <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
592
+ xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
593
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
594
+ xmlns:sec="http://www.sec.co.kr/">
595
+ <item restricted="1">
596
+ <dc:title>Video Item Title</dc:title>
597
+ <upnp:class>object.item.videoItem</upnp:class>
598
+ <upnp:albumArtURI>extra_property</upnp:albumArtURI>
599
+ </item>
600
+ </DIDL-Lite>"""
601
+ items = didl_lite.from_xml_string(didl_string)
602
+ assert len(items) == 1
603
+
604
+ item = items[0]
605
+ assert hasattr(item, "album_art_uri")
606
+ assert getattr(item, "album_art_uri") == "extra_property"
607
+ assert getattr(item, "albumArtURI") == "extra_property"
608
+
609
+ def test_default_properties_set(self) -> None:
610
+ """Test defaults for item properties."""
611
+ item = didl_lite.VideoItem(
612
+ id="0", parent_id="0", title="Video Item Title", restricted="1"
613
+ )
614
+ assert hasattr(item, "genre_type") # property is set
615
+
616
+ def test_property_case(self) -> None:
617
+ """Test item properties can be accessed using snake_case or camelCase."""
618
+ item = didl_lite.MusicTrack(
619
+ id="0",
620
+ parent_id="0",
621
+ title="Audio Item Title",
622
+ restricted="1",
623
+ language="English",
624
+ originalTrackNumber="1",
625
+ storage_medium="HDD",
626
+ )
627
+ assert hasattr(item, "original_track_number")
628
+ assert hasattr(item, "originalTrackNumber")
629
+ assert item.original_track_number is item.originalTrackNumber
630
+ assert item.original_track_number == "1"
631
+
632
+ assert hasattr(item, "storage_medium")
633
+ assert hasattr(item, "storageMedium")
634
+ assert item.storage_medium is item.storageMedium
635
+ assert item.storage_medium == "HDD"
636
+
637
+ assert hasattr(item, "long_description")
638
+ assert hasattr(item, "longDescription")
639
+ assert item.long_description is item.longDescription
640
+ assert item.long_description is None
641
+
642
+ assert not hasattr(item, "otherItem")
643
+ assert not hasattr(item, "other_item")
644
+
645
+ item.storageMedium = "CD"
646
+ assert item.storage_medium is item.storageMedium
647
+ assert item.storage_medium == "CD"
648
+
649
+ item.long_description = "Long description"
650
+ assert item.long_description is item.longDescription
651
+ assert item.long_description == "Long description"
652
+
653
+ item.otherItem = "otherItem"
654
+ assert hasattr(item, "otherItem")
655
+ assert not hasattr(item, "other_item")
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)
@@ -1,41 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: python-didl-lite
3
- Version: 1.3.2
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
- Description: DIDL-Lite (Digital Item Declaration Language) tools for Python
10
- ==============================================================
11
-
12
- DIDL-Lite tools for Python to read and write DIDL-Lite-xml.
13
-
14
- Usage
15
- -----
16
-
17
- See ``tests/`` for examples on how to use this.
18
-
19
- 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.
20
-
21
-
22
- Resources / documents
23
- ---------------------
24
-
25
- DIDL-Lite resources and documents:
26
-
27
- * [UPnP-av-ContentDirectory-v1-Service](http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf)
28
- * [UPnP-av-ContentDirectory-v2-Service](http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf)
29
- * [didl-lite-v2.xsd](http://www.upnp.org/schemas/av/didl-lite-v2.xsd)
30
- * [mpeg21-didl](http://xml.coverpages.org/mpeg21-didl.html)
31
-
32
- Platform: UNKNOWN
33
- Classifier: Development Status :: 5 - Production/Stable
34
- Classifier: Intended Audience :: Developers
35
- Classifier: License :: OSI Approved :: Apache Software License
36
- Classifier: Programming Language :: Python
37
- Classifier: Programming Language :: Python :: 3.6
38
- Classifier: Programming Language :: Python :: 3.7
39
- Classifier: Programming Language :: Python :: 3.8
40
- Classifier: Programming Language :: Python :: 3.9
41
- Description-Content-Type: text/x-rst
@@ -1,41 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: python-didl-lite
3
- Version: 1.3.2
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
- Description: DIDL-Lite (Digital Item Declaration Language) tools for Python
10
- ==============================================================
11
-
12
- DIDL-Lite tools for Python to read and write DIDL-Lite-xml.
13
-
14
- Usage
15
- -----
16
-
17
- See ``tests/`` for examples on how to use this.
18
-
19
- 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.
20
-
21
-
22
- Resources / documents
23
- ---------------------
24
-
25
- DIDL-Lite resources and documents:
26
-
27
- * [UPnP-av-ContentDirectory-v1-Service](http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf)
28
- * [UPnP-av-ContentDirectory-v2-Service](http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf)
29
- * [didl-lite-v2.xsd](http://www.upnp.org/schemas/av/didl-lite-v2.xsd)
30
- * [mpeg21-didl](http://xml.coverpages.org/mpeg21-didl.html)
31
-
32
- Platform: UNKNOWN
33
- Classifier: Development Status :: 5 - Production/Stable
34
- Classifier: Intended Audience :: Developers
35
- Classifier: License :: OSI Approved :: Apache Software License
36
- Classifier: Programming Language :: Python
37
- Classifier: Programming Language :: Python :: 3.6
38
- Classifier: Programming Language :: Python :: 3.7
39
- Classifier: Programming Language :: Python :: 3.8
40
- Classifier: Programming Language :: Python :: 3.9
41
- Description-Content-Type: text/x-rst