appstream-python 0.8__tar.gz → 0.9.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.
Files changed (26) hide show
  1. {appstream-python-0.8 → appstream_python-0.9.0}/LICENSE +1 -1
  2. {appstream-python-0.8/appstream_python.egg-info → appstream_python-0.9.0}/PKG-INFO +9 -7
  3. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python/Collection.py +23 -6
  4. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python/Component.py +152 -84
  5. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python/Release.py +41 -26
  6. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python/Shared.py +58 -45
  7. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python/StandardConstants.py +20 -3
  8. appstream_python-0.9.0/appstream_python/py.typed +0 -0
  9. appstream_python-0.9.0/appstream_python/version.txt +1 -0
  10. {appstream-python-0.8 → appstream_python-0.9.0/appstream_python.egg-info}/PKG-INFO +9 -7
  11. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python.egg-info/SOURCES.txt +2 -0
  12. {appstream-python-0.8 → appstream_python-0.9.0}/pyproject.toml +10 -6
  13. {appstream-python-0.8 → appstream_python-0.9.0}/tests/test_appstream_data.py +2 -0
  14. appstream_python-0.9.0/tests/test_collection.py +92 -0
  15. appstream-python-0.8/appstream_python/version.txt +0 -1
  16. {appstream-python-0.8 → appstream_python-0.9.0}/MANIFEST.in +0 -0
  17. {appstream-python-0.8 → appstream_python-0.9.0}/README.md +0 -0
  18. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python/__init__.py +0 -0
  19. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python/_helper.py +0 -0
  20. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python.egg-info/dependency_links.txt +0 -0
  21. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python.egg-info/requires.txt +1 -1
  22. {appstream-python-0.8 → appstream_python-0.9.0}/appstream_python.egg-info/top_level.txt +0 -0
  23. {appstream-python-0.8 → appstream_python-0.9.0}/setup.cfg +0 -0
  24. {appstream-python-0.8 → appstream_python-0.9.0}/tests/test_description.py +0 -0
  25. {appstream-python-0.8 → appstream_python-0.9.0}/tests/test_display_length.py +0 -0
  26. {appstream-python-0.8 → appstream_python-0.9.0}/tests/test_releases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  BSD 2-Clause License
2
2
 
3
- Copyright (c) 2022, JakobDev
3
+ Copyright (c) 2022-2026, JakobDev
4
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: appstream-python
3
- Version: 0.8
3
+ Version: 0.9.0
4
4
  Summary: A library for dealing with Freedesktop Appstream data
5
5
  Author-email: JakobDev <jakobdev@gmx.de>
6
- License: BSD-2-Clause
6
+ License-Expression: BSD-2-Clause
7
7
  Project-URL: Documentation, https://appstream-python.readthedocs.io
8
8
  Project-URL: Issues, https://codeberg.org/JakobDev/appstream-python/issues
9
9
  Project-URL: Source, https://codeberg.org/JakobDev/appstream-python
@@ -12,20 +12,22 @@ Keywords: JakobDev,AppStream,Freedesktop,Linux
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Environment :: Other Environment
15
- Classifier: License :: OSI Approved :: BSD License
16
15
  Classifier: Operating System :: POSIX
17
16
  Classifier: Operating System :: POSIX :: BSD
18
17
  Classifier: Operating System :: POSIX :: Linux
19
18
  Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.10
21
19
  Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
22
23
  Classifier: Programming Language :: Python :: 3 :: Only
23
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
- Requires-Python: >=3.10
25
+ Requires-Python: >=3.11
25
26
  Description-Content-Type: text/markdown
26
27
  License-File: LICENSE
27
- Requires-Dist: requests
28
28
  Requires-Dist: lxml
29
+ Requires-Dist: requests
30
+ Dynamic: license-file
29
31
 
30
32
  # appstream-python
31
33
 
@@ -5,20 +5,32 @@ import gzip
5
5
 
6
6
  class AppstreamCollection:
7
7
  "Represents a Collection of multiple AppStream files"
8
+
8
9
  def __init__(self) -> None:
9
10
  self._components: dict[str, AppstreamComponent] = {}
10
11
  self._categories: dict[str, list[str]] = {}
11
12
 
12
- def _add_appstream_tag(self, tag: etree.Element) -> None:
13
+ def _add_appstream_tag(self, tag: etree._Element) -> None:
13
14
  component_data = AppstreamComponent()
14
15
  component_data.parse_component_tag(tag)
16
+ self.add_component(component_data)
15
17
 
16
- self._components[component_data.id] = component_data
18
+ def add_component(self, component: AppstreamComponent) -> None:
19
+ "Adds a AppstreamComponent to the collection"
20
+ self._components[component.id] = component
17
21
 
18
- for i in component_data.categories:
22
+ for i in component.categories:
19
23
  if i not in self._categories:
20
24
  self._categories[i] = []
21
- self._categories[i].append(component_data.id)
25
+ self._categories[i].append(component.id)
26
+
27
+ def load_uncompressed_appstream_collection(self, path: str) -> None:
28
+ "Loads a uncompressed collection"
29
+ with open(path, "rb") as f:
30
+ root = etree.fromstring(f.read())
31
+
32
+ for i in root.findall("component"):
33
+ self._add_appstream_tag(i)
22
34
 
23
35
  def load_compressed_appstream_collection(self, path: str) -> None:
24
36
  "Loads a GZIP compressed collection"
@@ -43,7 +55,7 @@ class AppstreamCollection:
43
55
  """Returns a list with all available component id's"""
44
56
  return list(self._components.keys())
45
57
 
46
- def get_component(self, component_id: str) -> AppstreamComponent:
58
+ def get_component(self, component_id: str) -> AppstreamComponent | None:
47
59
  """Returns the component with the given id"""
48
60
  return self._components.get(component_id, None)
49
61
 
@@ -58,7 +70,7 @@ class AppstreamCollection:
58
70
 
59
71
  return category_list
60
72
 
61
- def get_collection_tag(self) -> etree.Element:
73
+ def get_collection_tag(self) -> etree._Element:
62
74
  "Gets the XML from the Collection"
63
75
  components_tag = etree.Element("components")
64
76
 
@@ -72,5 +84,10 @@ class AppstreamCollection:
72
84
  with open(path, "w", encoding="utf-8") as f:
73
85
  f.write(etree.tostring(self.get_collection_tag(), pretty_print=True, xml_declaration=True, encoding="utf-8").decode("utf-8"))
74
86
 
87
+ def write_compressed_file(self, path: str) -> None:
88
+ "Writes a Uncompressed collection file"
89
+ with gzip.open(path, "wb") as f:
90
+ f.write(etree.tostring(self.get_collection_tag(), pretty_print=True, xml_declaration=True, encoding="utf-8"))
91
+
75
92
  def __len__(self) -> int:
76
93
  return len(self._components)
@@ -1,11 +1,10 @@
1
1
  from .Shared import TranslateableTag, TranslateableList, Description
2
- from typing import Any, Optional, Literal, TypedDict, Union
2
+ from typing import Any, IO, Optional, Self, TypedDict, Union, cast
3
3
  from .Release import ReleaseList
4
4
  from ._helper import assert_func
5
5
  from .StandardConstants import *
6
6
  from lxml import etree
7
7
  import dataclasses
8
- import io
9
8
  import os
10
9
 
11
10
 
@@ -33,6 +32,7 @@ def _compare_relation_value(operator: RELATION_COMPARISON_OPERATOR_LITERAL, firs
33
32
 
34
33
  class InternetRelationDict(TypedDict):
35
34
  "the type for the content of the Internet attribute"
35
+
36
36
  value: INTERNET_RELATION_VALUE_LITERAL
37
37
  bandwidth_mbitps: Optional[int]
38
38
 
@@ -44,7 +44,7 @@ class Image:
44
44
  self.url: str = ""
45
45
  "The image URL"
46
46
 
47
- self.type: Literal["source", "thumbnail"] = "source"
47
+ self.type: IMAGE_TYPE_LITERAL = "source"
48
48
  "The image type"
49
49
 
50
50
  self.width: Optional[int] = None
@@ -56,22 +56,25 @@ class Image:
56
56
  self.language: Optional[str] = None
57
57
  "The language"
58
58
 
59
- def load_tag(self, tag: etree.Element) -> None:
59
+ def load_tag(self, tag: etree._Element) -> None:
60
60
  "Loads a image tag"
61
- self.url = tag.text.strip()
62
- self.type = tag.get("type", "source")
61
+ self.url = (tag.text or "").strip()
62
+ tag_type = tag.get("type", "source")
63
+ if tag_type not in IMAGE_TYPE:
64
+ raise ValueError(f"Invalid image type {tag_type}")
65
+ self.type = cast(IMAGE_TYPE_LITERAL, tag_type)
63
66
 
64
- try:
65
- self.width = int(tag.get("width"))
66
- except (ValueError, TypeError):
67
- self.width = None
67
+ tag_width = tag.get("width")
68
+ self.width = None
69
+ if tag_width is not None:
70
+ self.width = int(tag_width)
68
71
 
69
- try:
70
- self.height = int(tag.get("height"))
71
- except (ValueError, TypeError):
72
- self.height = None
72
+ tag_height = tag.get("height")
73
+ self.height = None
74
+ if tag_height is not None:
75
+ self.height = int(tag_height)
73
76
 
74
- self.language = tag.get("{http://www.w3.org/XML/1998/namespace}lang")
77
+ self.language = tag.get(_XML_LANG)
75
78
 
76
79
 
77
80
  class Screenshot:
@@ -95,7 +98,7 @@ class Screenshot:
95
98
  "Returns the thumbnail images"
96
99
  return [image for image in self.images if image.type == "thumbnail"]
97
100
 
98
- def load_tag(self, tag: etree.Element) -> None:
101
+ def load_tag(self, tag: etree._Element) -> None:
99
102
  "Load a screenshot tag"
100
103
  for i in tag.findall("image"):
101
104
  img = Image()
@@ -112,7 +115,9 @@ class DisplayLength:
112
115
  self.px: int = px
113
116
  "The logical pixels"
114
117
 
115
- self.compare: str = compare
118
+ if compare not in RELATION_COMPARISON_OPERATOR:
119
+ raise ValueError(f"Invalid compare {compare}")
120
+ self.compare: RELATION_COMPARISON_OPERATOR_LITERAL = cast(RELATION_COMPARISON_OPERATOR_LITERAL, compare)
116
121
  "Compare"
117
122
 
118
123
  def compare_px(self, value: int) -> bool:
@@ -124,7 +129,7 @@ class DisplayLength:
124
129
  """
125
130
  return _compare_relation_value(self.compare, value, self.px)
126
131
 
127
- def get_tag(self) -> etree.Element:
132
+ def get_tag(self) -> etree._Element:
128
133
  """
129
134
  Returns the XML Tag
130
135
 
@@ -139,16 +144,22 @@ class DisplayLength:
139
144
  return tag
140
145
 
141
146
  @classmethod
142
- def from_tag(cls: "DisplayLength", tag: etree.Element) -> "DisplayLength":
147
+ def from_tag(cls, tag: etree._Element) -> Self:
143
148
  "Creates the Object from an XML Tag"
144
149
  display_length = cls()
145
150
 
151
+ if tag.text is None:
152
+ raise ValueError("Missing text for tag {tag}")
153
+
146
154
  try:
147
155
  display_length.px = int(tag.text)
148
156
  except ValueError:
149
157
  display_length.px = cls.string_to_px(tag.text)
150
158
 
151
- display_length.compare = tag.get("compare", "ge")
159
+ tag_compare = tag.get("compare", "ge")
160
+ if tag_compare not in RELATION_COMPARISON_OPERATOR:
161
+ raise ValueError(f"Invalid compare {tag_compare}")
162
+ display_length.compare = cast(RELATION_COMPARISON_OPERATOR_LITERAL, tag_compare)
152
163
 
153
164
  return display_length
154
165
 
@@ -198,13 +209,13 @@ class Developer:
198
209
  id: Optional[str] = None
199
210
  name: TranslateableTag = dataclasses.field(default_factory=lambda: TranslateableTag())
200
211
 
201
- def load_tag(self, tag: etree.Element) -> None:
212
+ def load_tag(self, tag: etree._Element) -> None:
202
213
  "Loads the data from a XML tag"
203
214
  self.id = tag.get("id")
204
215
 
205
216
  self.name.load_tags(tag.findall("name"))
206
217
 
207
- def get_tag(self) -> etree.Element:
218
+ def get_tag(self) -> etree._Element:
208
219
  """
209
220
  Returns the XML Tag
210
221
 
@@ -260,7 +271,7 @@ class AppstreamComponent:
260
271
  self.urls: dict[URL_TYPES_LITERAL, str] = {}
261
272
  "The URLs"
262
273
 
263
- self.launchables: dict[LAUNCHABLE_TYPES, str] = {}
274
+ self.launchables: dict[LAUNCHABLE_TYPES_LITERAL, str] = {}
264
275
  "The launchables"
265
276
 
266
277
  self.oars: dict[OARS_ATTRIBUTE_TYPES_LITERAL, OARS_VALUE_TYPES_LITERAL] = {}
@@ -290,13 +301,13 @@ class AppstreamComponent:
290
301
  self.keywords: TranslateableList = TranslateableList()
291
302
  "The Keywords"
292
303
 
293
- self.controls: dict[CONTROL_TYPES_LITERAL, Optional[Literal["requires", "recommends", "supports"]]] = {}
304
+ self.controls: dict[CONTROL_TYPES_LITERAL, Optional[RELATION_LITERAL]] = {}
294
305
  "The Controls"
295
306
 
296
- self.display_length: dict[Literal["requires", "recommends", "supports"], list[DisplayLength]] = {}
307
+ self.display_length: dict[RELATION_LITERAL, list[DisplayLength]] = {}
297
308
  "The Display Length"
298
309
 
299
- self.internet: dict[Literal["requires", "recommends", "supports"], InternetRelationDict] = {}
310
+ self.internet: dict[RELATION_LITERAL, InternetRelationDict] = {}
300
311
 
301
312
  self.kudos: list[str] = []
302
313
  "The Kudos"
@@ -357,16 +368,23 @@ class AppstreamComponent:
357
368
 
358
369
  def get_available_languages(self) -> list[str]:
359
370
  "Returns a list with all available languages of the Component"
360
- lang_list = self.name.get_available_languages() + self.summary.get_available_languages() + self.developer_name.get_available_languages()
371
+ lang_list = self.name.get_available_languages() + self.summary.get_available_languages() + self.developer.name.get_available_languages()
361
372
  return list(set(lang_list))
362
373
 
363
- def _parse_relation_tag(self, tag: etree.Element) -> None:
374
+ def _parse_relation_tag(self, tag: etree._Element) -> None:
364
375
  "Parses a relation tag"
365
376
  relation = tag.tag
377
+ if relation not in RELATION:
378
+ raise ValueError(f"Invalid relation {relation}")
379
+ relation = cast(RELATION_LITERAL, relation)
366
380
 
367
381
  for control_tag in tag.findall("control"):
368
- if control_tag.text.strip() in CONTROL_TYPES:
369
- self.controls[control_tag.text.strip()] = relation
382
+ if control_tag.text is None:
383
+ raise ValueError("Empty control tag text {control_tag}")
384
+ control_tag_text = control_tag.text.strip()
385
+ if control_tag_text not in CONTROL_TYPES:
386
+ raise ValueError("Invalid control tag {control_tag_text}")
387
+ self.controls[cast(CONTROL_TYPES_LITERAL, control_tag_text)] = relation
370
388
 
371
389
  for display_length_tag in tag.findall("display_length"):
372
390
  if relation in self.display_length:
@@ -376,23 +394,31 @@ class AppstreamComponent:
376
394
 
377
395
  internet_tag = tag.find("internet")
378
396
  if internet_tag is not None:
379
- internet_dict: InternetRelationDict = {"value": internet_tag.text.strip()}
397
+ internet_tag_text = (internet_tag.text or "").strip()
398
+ if internet_tag_text not in INTERNET_RELATION_VALUE:
399
+ raise ValueError(f"Invalid internet relation {internet_tag_text}")
400
+ internet_dict: InternetRelationDict = {"bandwidth_mbitps": None, "value": cast(INTERNET_RELATION_VALUE_LITERAL, internet_tag_text)}
380
401
 
381
- try:
382
- internet_dict["bandwidth_mbitps"] = int(internet_tag.get("bandwidth_mbitps"))
383
- except (ValueError, TypeError):
384
- internet_dict["bandwidth_mbitps"] = None
402
+ internet_bandwidth = internet_tag.get("bandwidth_mbitps")
403
+ if internet_bandwidth is not None:
404
+ try:
405
+ internet_dict["bandwidth_mbitps"] = int(internet_bandwidth)
406
+ except ValueError:
407
+ internet_dict["bandwidth_mbitps"] = None
385
408
 
386
409
  self.internet[relation] = internet_dict
387
410
 
388
- def parse_component_tag(self, tag: etree._ElementTree) -> None:
411
+ def parse_component_tag(self, tag: etree._Element) -> None:
389
412
  "Parses a XML tag"
390
- self.id = tag.find("id").text.strip()
413
+ id = tag.find("id")
414
+ if id is not None and id.text is not None:
415
+ self.id = id.text.strip()
391
416
 
392
- try:
393
- self.type = tag.xpath("/component")[0].get("type")
394
- except IndexError:
395
- self.type = tag.get("type")
417
+ tag_component = tag.xpath("/component")
418
+ if isinstance(tag_component, list) and len(tag_component) > 0:
419
+ self.type = cast(etree._Element, tag_component[0]).get("type", "")
420
+ else:
421
+ self.type = tag.get("type", "")
396
422
 
397
423
  self.name.load_tags(tag.findall("name"))
398
424
 
@@ -409,42 +435,65 @@ class AppstreamComponent:
409
435
  self.description.load_tags(description_tag)
410
436
 
411
437
  metadata_license_tag = tag.find("metadata_license")
412
- if metadata_license_tag is not None:
438
+ if metadata_license_tag is not None and metadata_license_tag.text is not None:
413
439
  self.metadata_license = metadata_license_tag.text.strip()
414
440
 
415
441
  project_license_tag = tag.find("project_license")
416
- if project_license_tag is not None:
442
+ if project_license_tag is not None and project_license_tag.text is not None:
417
443
  self.project_license = project_license_tag.text.strip()
418
444
 
419
445
  categories_tag = tag.find("categories")
420
446
  if categories_tag is not None:
421
447
  for i in categories_tag.findall("category"):
448
+ if i.text is None:
449
+ raise ValueError("Empty text for category tag {i}")
422
450
  self.categories.append(i.text.strip())
423
451
 
424
452
  for i in tag.findall("url"):
425
- if i.get("type") in URL_TYPES:
426
- self.urls[i.get("type")] = i.text.strip()
453
+ if i.text is None:
454
+ raise ValueError("Empty text for url tag {i}")
455
+ i_type = i.get("type")
456
+ if i_type not in URL_TYPES:
457
+ raise ValueError("Invalid url type {i_type}")
458
+ self.urls[cast(URL_TYPES_LITERAL, i_type)] = i.text.strip()
427
459
 
428
460
  for i in tag.findall("launchable"):
429
- if i.get("type") in LAUNCHABLE_TYPES:
430
- self.launchables[i.get("type")] = i.text.strip()
461
+ if i.text is None:
462
+ raise ValueError("Empty text for launchable tag {i}")
463
+ i_type = i.get("type")
464
+ if i_type not in LAUNCHABLE_TYPES:
465
+ raise ValueError("Invalid launchable type {i_type}")
466
+ self.launchables[cast(LAUNCHABLE_TYPES_LITERAL, i_type)] = i.text.strip()
431
467
 
432
468
  oars_tag = tag.find("content_rating")
433
469
  if oars_tag is not None:
434
470
  for i in oars_tag.findall("content_attribute"):
435
- if i.get("id") in OARS_ATTRIBUTE_TYPES and i.text.strip() in OARS_VALUE_TYPES:
436
- self.oars[i.get("id")] = i.text.strip()
471
+ i_id_attr = i.get("id")
472
+ if i_id_attr is None:
473
+ raise ValueError("Missing id attribute for content_attribute tag {i}")
474
+ if i.text is None:
475
+ raise ValueError("Empty text for content_attribute tag {i}")
476
+ i_id_val = i.text.strip()
477
+ if i_id_attr not in OARS_ATTRIBUTE_TYPES:
478
+ raise ValueError("Invalid id attribute {i_id_attr}")
479
+ if i_id_val not in OARS_VALUE_TYPES:
480
+ raise ValueError("Invalid id value {i_id_val}")
481
+ self.oars[cast(OARS_ATTRIBUTE_TYPES_LITERAL, i_id_attr)] = cast(OARS_VALUE_TYPES_LITERAL, i_id_val)
437
482
 
438
483
  provides_tag = tag.find("provides")
439
484
  if provides_tag is not None:
440
- for i in provides_tag.getchildren():
485
+ for i in provides_tag:
486
+ if i.text is None:
487
+ raise ValueError("Empty text for provides tag {i}")
441
488
  if i.tag in PROVIDES_TYPES:
442
- self.provides[i.tag].append(i.text.strip())
489
+ self.provides[cast(PROVIDES_TYPES_LITERAL, i.tag)].append(i.text.strip())
443
490
 
444
491
  # For backwards compatibility. See: https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-mimetypes
445
492
  mimetypes_tag = tag.find("mimetypes")
446
493
  if mimetypes_tag is not None:
447
494
  for i in mimetypes_tag.findall("mimetype"):
495
+ if i.text is None:
496
+ raise ValueError("Empty text for mimetypes tag {i}")
448
497
  self.provides["mediatype"].append(i.text.strip())
449
498
 
450
499
  releases_tag = tag.find("releases")
@@ -460,20 +509,27 @@ class AppstreamComponent:
460
509
 
461
510
  project_group_tag = tag.find("project_group")
462
511
  if project_group_tag is not None:
512
+ if project_group_tag.text is None:
513
+ raise ValueError("Empty text for project_group tag {project_group_tag}")
463
514
  self.project_group = project_group_tag.text.strip()
464
515
 
465
516
  for i in tag.findall("translation"):
466
- trans_dict = {}
467
- trans_dict["type"] = i.get("type")
468
517
  if i.text is None:
469
- trans_dict["value"] = ""
470
- else:
471
- trans_dict["value"] = i.text.strip()
518
+ continue
519
+ trans_dict = {
520
+ "type": i.get("type", ""),
521
+ "value": "",
522
+ }
523
+ if i.text is None:
524
+ raise ValueError("Empty text for lang tag {i}")
525
+ trans_dict["value"] = i.text.strip()
472
526
  self.translation.append(trans_dict)
473
527
 
474
528
  languages_tag = tag.find("languages")
475
529
  if languages_tag is not None:
476
530
  for i in languages_tag.findall("lang"):
531
+ if i.text is None:
532
+ continue
477
533
  try:
478
534
  self.languages[i.text.strip()] = int(i.get("percentage") or 100)
479
535
  except ValueError:
@@ -497,45 +553,57 @@ class AppstreamComponent:
497
553
  kudos_tag = tag.find("kudos")
498
554
  if kudos_tag is not None:
499
555
  for i in kudos_tag.findall("kudo"):
556
+ if i.text is None:
557
+ raise ValueError("Empty text for kudo tag {i}")
500
558
  self.kudos.append(i.text.strip())
501
559
 
502
560
  update_contact_tag = tag.find("update_contact")
503
561
  if update_contact_tag is not None:
562
+ if update_contact_tag.text is None:
563
+ raise ValueError("Empty text for update_contact tag {update_contact_tag}")
504
564
  self.update_contact = update_contact_tag.text.strip()
505
565
 
506
566
  replaces_tag = tag.find("replaces")
507
567
  if replaces_tag is not None:
508
568
  for i in replaces_tag.findall("id"):
569
+ if i.text is None:
570
+ raise ValueError("Empty text for replaces tag {replaces_tag}")
509
571
  self.replaces.append(i.text)
510
572
 
511
573
  suggests_tag = tag.find("suggests")
512
574
  if suggests_tag is not None:
513
575
  for i in suggests_tag.findall("id"):
576
+ if i.text is None:
577
+ raise ValueError("Empty text for suggests tag {suggests_tag}")
514
578
  self.suggests.append(i.text)
515
579
 
516
580
  custom_tag = tag.find("custom")
517
581
  if custom_tag is not None:
518
582
  for i in custom_tag.findall("value"):
583
+ if i.text is None:
584
+ raise ValueError("Empty text for custom tag {custom_tag}")
519
585
  if key := i.get("key", "").strip():
520
586
  self.custom[key] = i.text.strip()
521
587
 
522
588
  for i in tag.findall("extends"):
589
+ if i.text is None:
590
+ raise ValueError("Empty text for extends tag {i}")
523
591
  self.extends.append(i.text.strip())
524
592
 
525
593
  @classmethod
526
- def from_component_tag(cls: "AppstreamComponent", root: etree._Element) -> "AppstreamComponent":
594
+ def from_component_tag(cls, root: etree._Element) -> Self:
527
595
  "Load an appdata.xml or metainfo.xml file"
528
596
  component = cls()
529
597
  component.parse_component_tag(root)
530
598
  return component
531
599
 
532
- def load_file(self, path: Union[str, os.PathLike, io.RawIOBase]) -> None:
600
+ def load_file(self, path: Union[str, os.PathLike, IO[Any]]) -> None:
533
601
  """Load an appdata.xml or metainfo.xml file"""
534
602
  root = etree.parse(path)
535
- self.parse_component_tag(root)
603
+ self.parse_component_tag(cast(etree._Element, root))
536
604
 
537
605
  @classmethod
538
- def from_file(cls: "AppstreamComponent", path: Union[str, os.PathLike, io.RawIOBase]) -> "AppstreamComponent":
606
+ def from_file(cls, path: Union[str, os.PathLike, IO[Any]]) -> Self:
539
607
  "Load an appdata.xml or metainfo.xml file"
540
608
  component = cls()
541
609
  component.load_file(path)
@@ -547,7 +615,7 @@ class AppstreamComponent:
547
615
  self.parse_component_tag(root)
548
616
 
549
617
  @classmethod
550
- def from_bytes(cls: "AppstreamComponent", data: bytes, encoding: Optional[str] = None) -> "AppstreamComponent":
618
+ def from_bytes(cls, data: bytes, encoding: Optional[str] = None) -> Self:
551
619
  "Load an appdata.xml or metainfo.xml byte string"
552
620
  component = cls()
553
621
  component.load_bytes(data, encoding)
@@ -558,13 +626,13 @@ class AppstreamComponent:
558
626
  self.load_bytes(text.encode("utf-8"), encoding="utf-8")
559
627
 
560
628
  @classmethod
561
- def from_string(cls: "AppstreamComponent", text: str) -> "AppstreamComponent":
629
+ def from_string(cls, text: str) -> Self:
562
630
  "Load an appdata.xml or metainfo.xml string"
563
631
  component = cls()
564
632
  component.load_string(text)
565
633
  return component
566
634
 
567
- def _get_relation_tag(self, parent_tag: etree.Element, relation: Literal["supports", "recommends", "requires"]) -> None:
635
+ def _get_relation_tag(self, parent_tag: etree._Element, relation: RELATION_LITERAL) -> None:
568
636
  "Craetes a relation tag from the Component."
569
637
  relation_tag = etree.SubElement(parent_tag, relation)
570
638
 
@@ -584,10 +652,10 @@ class AppstreamComponent:
584
652
 
585
653
  internet_tag.text = self.internet[relation]["value"]
586
654
 
587
- if len(relation_tag.getchildren()) == 0:
655
+ if len(relation_tag) == 0:
588
656
  parent_tag.remove(relation_tag)
589
657
 
590
- def get_component_tag(self) -> etree.Element:
658
+ def get_component_tag(self) -> etree._Element:
591
659
  "Creates a XML tag from the Component"
592
660
  tag = etree.Element("component")
593
661
  tag.set("type", self.type)
@@ -610,16 +678,16 @@ class AppstreamComponent:
610
678
  project_license_tag = etree.SubElement(tag, "project_license")
611
679
  project_license_tag.text = self.project_license
612
680
 
613
- for key, value in self.urls.items():
614
- if key in URL_TYPES:
681
+ for key_url, value in self.urls.items():
682
+ if key_url in URL_TYPES:
615
683
  url_tag = etree.SubElement(tag, "url")
616
- url_tag.set("type", key)
684
+ url_tag.set("type", key_url)
617
685
  url_tag.text = value
618
686
 
619
- for key, value in self.launchables.items():
620
- if key in LAUNCHABLE_TYPES:
687
+ for key_launchable, value in self.launchables.items():
688
+ if key_launchable in LAUNCHABLE_TYPES:
621
689
  url_tag = etree.SubElement(tag, "launchable")
622
- url_tag.set("type", key)
690
+ url_tag.set("type", key_launchable)
623
691
  url_tag.text = value
624
692
 
625
693
  oars_tag = etree.SubElement(tag, "content_rating")
@@ -637,15 +705,15 @@ class AppstreamComponent:
637
705
  single_categorie_tag.text = i
638
706
 
639
707
  provides_tag = etree.SubElement(tag, "provides")
640
- for key, value in self.provides.items():
641
- if key not in PROVIDES_TYPES:
708
+ for key_provides, value_provides in self.provides.items():
709
+ if key_provides not in PROVIDES_TYPES:
642
710
  continue
643
- for i in value:
644
- single_provides_tag = etree.SubElement(provides_tag, key)
711
+ for i in value_provides:
712
+ single_provides_tag = etree.SubElement(provides_tag, key_provides)
645
713
  single_provides_tag.text = i
646
714
 
647
715
  # Don't write empty provides tag
648
- if len(provides_tag.getchildren()) == 0:
716
+ if len(provides_tag) == 0:
649
717
  tag.remove(provides_tag)
650
718
 
651
719
  if len(self.releases) != 0:
@@ -655,17 +723,17 @@ class AppstreamComponent:
655
723
  project_group_tag = etree.SubElement(tag, "project_group")
656
724
  project_group_tag.text = self.project_group
657
725
 
658
- for i in self.translation:
726
+ for i_translation in self.translation:
659
727
  translation_tag = etree.SubElement(tag, "translation")
660
- translation_tag.set("type", i["type"])
661
- translation_tag.text = i["value"]
728
+ translation_tag.set("type", i_translation["type"])
729
+ translation_tag.text = i_translation["value"]
662
730
 
663
731
  if len(self.languages) > 0:
664
732
  languages_tag = etree.SubElement(tag, "languages")
665
- for key, value in self.languages.items():
733
+ for key_languages, value_languages in self.languages.items():
666
734
  single_language_tag = etree.SubElement(languages_tag, "lang")
667
- single_language_tag.set("percentage", str(value))
668
- single_language_tag.text = key
735
+ single_language_tag.set("percentage", str(value_languages))
736
+ single_language_tag.text = key_languages
669
737
 
670
738
  self._get_relation_tag(tag, "supports")
671
739
  self._get_relation_tag(tag, "recommends")
@@ -695,9 +763,9 @@ class AppstreamComponent:
695
763
 
696
764
  if len(self.custom) > 0:
697
765
  custom_tag = etree.SubElement(tag, "custom")
698
- for key, value in self.custom:
766
+ for key_custom, value in self.custom.items():
699
767
  value_tag = etree.SubElement(custom_tag, "value")
700
- value_tag.set("key", key.strip())
768
+ value_tag.set("key", key_custom.strip())
701
769
  value_tag.text = value.strip()
702
770
 
703
771
  for i in self.extends:
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
- from typing import Type, Literal
2
+ from typing import Literal, Self, cast, get_args
3
3
  from ._helper import assert_func
4
4
  from .Shared import Description
5
5
  from lxml import etree
@@ -29,7 +29,7 @@ class Release:
29
29
  def __post_init__(self) -> None:
30
30
  self.description = copy.deepcopy(self.description)
31
31
 
32
- def get_tag(self) -> etree.Element:
32
+ def get_tag(self) -> etree._Element:
33
33
  """
34
34
  Returns the XML Tag
35
35
 
@@ -53,28 +53,40 @@ class Release:
53
53
  return tag
54
54
 
55
55
  @classmethod
56
- def from_tag(cls: Type["Release"], tag: etree._Element) -> "Release":
56
+ def from_tag(cls, tag: etree._Element) -> Self:
57
57
  "Loads a release tag"
58
58
  release = cls()
59
59
 
60
60
  release.version = tag.get("version", "")
61
- release.type = tag.get("type", "stable")
62
- release.urgency = tag.get("urgency", "unknown")
63
-
64
- try:
65
- release.date = datetime.datetime.fromisoformat(tag.get("date")).date()
66
- except Exception:
67
- pass
68
-
69
- try:
70
- release.date = datetime.date.fromtimestamp(int(tag.get("timestamp")))
71
- except Exception:
72
- pass
73
-
74
- try:
75
- release.date_eol = datetime.datetime.fromisoformat(tag.get("date_eol")).date()
76
- except Exception:
77
- pass
61
+ tag_type = tag.get("type", "stable")
62
+ if tag_type not in get_args(ReleaseType):
63
+ raise ValueError(f"Invalid type {tag_type}")
64
+ release.type = cast(ReleaseType, tag_type)
65
+ tag_urgency = tag.get("urgency", "unknown")
66
+ if tag_urgency not in get_args(UrgencyType):
67
+ raise ValueError(f"Invalid urgency {tag_urgency}")
68
+ release.urgency = cast(UrgencyType, tag_urgency)
69
+
70
+ tag_date = tag.get("date")
71
+ if tag_date is not None:
72
+ try:
73
+ release.date = datetime.datetime.fromisoformat(tag_date).date()
74
+ except Exception:
75
+ pass
76
+
77
+ tag_timestamp = tag.get("timestamp")
78
+ if tag_timestamp is not None:
79
+ try:
80
+ release.date = datetime.date.fromtimestamp(int(tag_timestamp))
81
+ except Exception:
82
+ pass
83
+
84
+ tag_date_eol = tag.get("date_eol")
85
+ if tag_date_eol is not None:
86
+ try:
87
+ release.date_eol = datetime.datetime.fromisoformat(tag_date_eol).date()
88
+ except Exception:
89
+ pass
78
90
 
79
91
  description_tag = tag.find("description")
80
92
  if description_tag is not None:
@@ -107,7 +119,7 @@ class ReleaseList(collections.UserList[Release]):
107
119
  for single_release in tag.findall("release"):
108
120
  self.append(Release.from_tag(single_release))
109
121
 
110
- def get_tag(self) -> etree.Element:
122
+ def get_tag(self) -> etree._Element:
111
123
  """
112
124
  Returns the XML Tag
113
125
 
@@ -140,11 +152,14 @@ class ReleaseList(collections.UserList[Release]):
140
152
  f.write(etree.tostring(self.get_tag(), pretty_print=True, xml_declaration=True, encoding="utf-8"))
141
153
 
142
154
  @classmethod
143
- def from_tag(cls: Type["ReleaseList"], tag: etree._Element, fetch_external: bool = False) -> "ReleaseList":
155
+ def from_tag(cls, tag: etree._Element, fetch_external: bool = False) -> Self:
144
156
  "Creates the list from an XMl tag"
145
157
  release_list = cls()
146
158
 
147
- release_list.type = tag.get("type", "embedded")
159
+ tag_type = tag.get("type", "embedded")
160
+ if tag_type not in get_args(ReleaseListType):
161
+ raise ValueError(f"Invalid type {tag_type}")
162
+ release_list.type = cast(ReleaseListType, tag_type)
148
163
  release_list.url = tag.get("url", "")
149
164
 
150
165
  for single_release in tag.findall("release"):
@@ -156,18 +171,18 @@ class ReleaseList(collections.UserList[Release]):
156
171
  return release_list
157
172
 
158
173
  @classmethod
159
- def from_string(cls: Type["ReleaseList"], text: str) -> "ReleaseList":
174
+ def from_string(cls, text: str) -> Self:
160
175
  "Loads the Releases from a string"
161
176
  return cls.from_tag(etree.fromstring(text.encode("utf-8")))
162
177
 
163
178
  @classmethod
164
- def from_file(cls: Type["ReleaseList"], path: str | os.PathLike) -> "ReleaseList":
179
+ def from_file(cls, path: str | os.PathLike) -> Self:
165
180
  "Loads the Releases from a file"
166
181
  with open(path, "r", encoding="utf-8") as f:
167
182
  return cls.from_string(f.read())
168
183
 
169
184
  @classmethod
170
- def from_url(cls: Type["ReleaseList"], url: str) -> "ReleaseList":
185
+ def from_url(cls, url: str) -> Self:
171
186
  "Loads the Releases from a URL"
172
187
  return cls.from_tag(etree.fromstring(requests.get(url).content))
173
188
 
@@ -8,6 +8,7 @@ _XML_LANG = "{http://www.w3.org/XML/1998/namespace}lang"
8
8
 
9
9
  class TranslateableTag:
10
10
  "Represents a translatable tag"
11
+
11
12
  def __init__(self) -> None:
12
13
  self._text = ""
13
14
  self._translations: dict[str, str] = {}
@@ -24,8 +25,10 @@ class TranslateableTag:
24
25
  """Returns the translated text"""
25
26
  return self._translations.get(lang, None)
26
27
 
27
- def get_translated_text_default(self, lang: str) -> Optional[str]:
28
+ def get_translated_text_default(self, lang: Optional[str]) -> Optional[str]:
28
29
  """Returns the translated text. Returns the default text, if the translation does not exists"""
30
+ if lang is None:
31
+ return self._text
29
32
  return self._translations.get(lang, self._text)
30
33
 
31
34
  def set_translated_text(self, lang: str, text: str) -> None:
@@ -36,28 +39,29 @@ class TranslateableTag:
36
39
  """Returns a list with all languages of the tag"""
37
40
  return list(self._translations.keys())
38
41
 
39
- def load_tags(self, tag_list: list[etree.Element]) -> None:
42
+ def load_tags(self, tag_list: list[etree._Element]) -> None:
40
43
  """Load a list of Tags"""
41
44
  for i in tag_list:
42
- if i.get("{http://www.w3.org/XML/1998/namespace}lang") is None:
45
+ i_lang = i.get(_XML_LANG)
46
+ if i_lang is None:
43
47
  if i.text is not None:
44
48
  self._text = i.text.strip()
45
49
  else:
46
50
  self._text = ""
47
51
  else:
48
52
  if i.text is not None:
49
- self._translations[i.get(_XML_LANG)] = i.text.strip()
53
+ self._translations[i_lang] = i.text.strip()
50
54
  else:
51
- self._translations[i.get(_XML_LANG)] = ""
55
+ self._translations[i_lang] = ""
52
56
 
53
- def write_tags(self, parent_tag: etree.Element, tag_name: str) -> None:
57
+ def write_tags(self, parent_tag: etree._Element, tag_name: str) -> None:
54
58
  """Writes a Tag"""
55
59
  default_tag = etree.SubElement(parent_tag, tag_name)
56
60
  default_tag.text = self._text
57
61
 
58
62
  for key, value in self._translations.items():
59
63
  translation_tag = etree.SubElement(parent_tag, tag_name)
60
- translation_tag.set("{http://www.w3.org/XML/1998/namespace}lang", key)
64
+ translation_tag.set(_XML_LANG, key)
61
65
  translation_tag.text = value
62
66
 
63
67
  def clear(self) -> None:
@@ -90,37 +94,43 @@ class TranslateableList:
90
94
  "Returns a list with the default items"
91
95
  return list(self._translated_data.keys())
92
96
 
93
- def get_translated_list(self, lang: str) -> list[str]:
97
+ def get_translated_list(self, lang: Optional[str]) -> list[str]:
94
98
  "Returns the translated list for the given language"
95
- if lang in self._translated_lists:
99
+ if lang is not None and lang in self._translated_lists:
96
100
  return self._translated_lists[lang]
97
101
 
98
102
  return_list: list[str] = []
99
103
  for untranslated_text, translations in self._translated_data.items():
100
- return_list.append(translations.get(lang, untranslated_text))
104
+ translated_text = untranslated_text
105
+ if lang is not None:
106
+ translated_text = translations.get(lang, untranslated_text)
107
+ return_list.append(translated_text)
101
108
  return return_list
102
109
 
103
- def load_tag(self, tag: etree.Element) -> None:
110
+ def load_tag(self, tag: etree._Element) -> None:
104
111
  "Loads an Tag. Only for internal use."
105
- if tag.get("{http://www.w3.org/XML/1998/namespace}lang") is None:
112
+ tag_lang = tag.get(_XML_LANG)
113
+ if tag_lang is None:
106
114
  current_text = ""
107
- for i in tag.getchildren():
108
- if i.get(_XML_LANG) is None:
109
- try:
110
- current_text = i.text.strip()
111
- self._translated_data[current_text] = {}
112
- except AttributeError:
113
- pass
115
+ for i in tag:
116
+ if i.text is None:
117
+ continue
118
+ i_lang = i.get(_XML_LANG)
119
+ if i_lang is None:
120
+ current_text = i.text.strip()
121
+ self._translated_data[current_text] = {}
114
122
  else:
115
123
  if current_text in self._translated_data:
116
- self._translated_data[current_text][i.get(_XML_LANG)] = i.text.strip()
124
+ self._translated_data[current_text][i_lang] = i.text.strip()
117
125
  else:
118
- if tag.get("{http://www.w3.org/XML/1998/namespace}lang") not in self._translated_lists:
119
- self._translated_lists[tag.get("{http://www.w3.org/XML/1998/namespace}lang")] = []
120
- for i in tag.getchildren():
121
- self._translated_lists[tag.get("{http://www.w3.org/XML/1998/namespace}lang")].append(i.text.strip())
122
-
123
- def write_all_tag(self, parent_tag: etree.Element, tag_name: str) -> None:
126
+ if tag_lang not in self._translated_lists:
127
+ self._translated_lists[tag_lang] = []
128
+ for i in tag:
129
+ if i.text is None:
130
+ continue
131
+ self._translated_lists[tag_lang].append(i.text.strip())
132
+
133
+ def write_all_tag(self, parent_tag: etree._Element, tag_name: str) -> None:
124
134
  "Writes the XML tags. Onnly for internal use."
125
135
  for untranslated_text, translations in self._translated_data.items():
126
136
  default_tag = etree.SubElement(parent_tag, tag_name)
@@ -130,13 +140,13 @@ class TranslateableList:
130
140
  translated_tag.set(_XML_LANG, lang)
131
141
  translated_tag.text = translated_text
132
142
 
133
- def write_untranslated_tags(self, parent_tag: etree.Element, tag_name: str) -> None:
143
+ def write_untranslated_tags(self, parent_tag: etree._Element, tag_name: str) -> None:
134
144
  "Writes the untranslated XML tags. Onnly for internal use."
135
145
  for untranslated_text in self._translated_data.keys():
136
146
  default_tag = etree.SubElement(parent_tag, tag_name)
137
147
  default_tag.text = untranslated_text
138
148
 
139
- def write_translated_tags(self, parent_tag: etree.Element, tag_name: str, lang: str) -> None:
149
+ def write_translated_tags(self, parent_tag: etree._Element, tag_name: str, lang: str) -> None:
140
150
  "Writes the translated XML tags. Onnly for internal use."
141
151
  for untranslated_text, translations in self._translated_data.items():
142
152
  child_tag = etree.SubElement(parent_tag, tag_name)
@@ -163,17 +173,17 @@ class DescriptionItem:
163
173
 
164
174
  def get_type(self) -> Literal["paragraph", "unordered-list", "ordered-list"]:
165
175
  "Retutns the Type of the Item"
166
- return "none"
176
+ raise NotImplementedError
167
177
 
168
- def load_tags(self, tag_list: list[etree.Element]) -> None:
178
+ def load_tags(self, tag_list: list[etree._Element]) -> None:
169
179
  "Loads teh XML tags into the Elemnt. Only for internal use."
170
180
  raise NotImplementedError
171
181
 
172
- def get_tags(self, parent_tag: etree.Element) -> None:
182
+ def get_tags(self, parent_tag: etree._Element) -> None:
173
183
  "Get the XML Tag from the Element. Only for internal use."
174
184
  raise NotImplementedError()
175
185
 
176
- def get_translated_tag(self, lang: Optional[str]) -> etree.Element:
186
+ def get_translated_tag(self, lang: Optional[str]) -> etree._Element:
177
187
  "Loads the tag for a given language"
178
188
  raise NotImplementedError()
179
189
 
@@ -193,15 +203,15 @@ class DescriptionParagraph(DescriptionItem):
193
203
  "Retutns the Type of the Item"
194
204
  return "paragraph"
195
205
 
196
- def load_tags(self, tag_list: list[etree.Element]) -> None:
206
+ def load_tags(self, tag_list: list[etree._Element]) -> None:
197
207
  "Loads teh XML tags into the Elemnt. Only for internal use."
198
208
  self.content.load_tags(tag_list)
199
209
 
200
- def get_tags(self, parent_tag: etree.Element) -> None:
210
+ def get_tags(self, parent_tag: etree._Element) -> None:
201
211
  "Get the XML Tag from the Element. Only for internal use."
202
212
  self.content.write_tags(parent_tag, "p")
203
213
 
204
- def get_translated_tag(self, lang: Optional[str]) -> etree.Element:
214
+ def get_translated_tag(self, lang: Optional[str]) -> etree._Element:
205
215
  "Loads the tag for a given language"
206
216
  paragraph_tag = etree.Element("p")
207
217
  if lang is None:
@@ -212,7 +222,7 @@ class DescriptionParagraph(DescriptionItem):
212
222
 
213
223
  def to_plain_text(self, lang: Optional[str] = None) -> str:
214
224
  "Returns the content as plain text"
215
- return self.content.get_translated_text_default(lang).strip()
225
+ return (self.content.get_translated_text_default(lang) or "").strip()
216
226
 
217
227
  def __repr__(self) -> str:
218
228
  return f"<DescriptionParagraph default='{self.content.get_default_text()}'>"
@@ -240,16 +250,19 @@ class DescriptionList(DescriptionItem):
240
250
  else:
241
251
  return "ordered-list"
242
252
 
243
- def load_tags(self, tag_list: list[etree.Element]) -> None:
253
+ def load_tags(self, tag_list: etree._Element | list[etree._Element]) -> None:
244
254
  "Loads the XML tags into the Elemnt. Only for internal use."
245
- self.content.load_tag(tag_list)
255
+ if not isinstance(tag_list, list):
256
+ tag_list = [tag_list]
257
+ for tag in tag_list:
258
+ self.content.load_tag(tag)
246
259
 
247
- def get_tags(self, parent_tag: etree.Element) -> None:
260
+ def get_tags(self, parent_tag: etree._Element) -> None:
248
261
  "Get the XML Tag from the Element. Only for internal use."
249
262
  list_tag = etree.SubElement(parent_tag, self._list_type)
250
263
  self.content.write_all_tag(list_tag, "li")
251
264
 
252
- def get_translated_tag(self, lang: Optional[str] = None) -> etree.Element:
265
+ def get_translated_tag(self, lang: Optional[str] = None) -> etree._Element:
253
266
  "Loads the tag for a given language"
254
267
  list_tag = etree.Element(self._list_type)
255
268
  if lang is None:
@@ -291,12 +304,12 @@ class Description:
291
304
  self.items: list[DescriptionItem] = []
292
305
  """All Description Items"""
293
306
 
294
- def load_tags(self, tag: etree.Element) -> None:
307
+ def load_tags(self, tag: etree._Element) -> None:
295
308
  "Load a XML tag. Onyl for internal use."
296
- paragraph_list: list[etree.Element] = []
297
- for i in tag.getchildren():
309
+ paragraph_list: list[etree._Element] = []
310
+ for i in tag:
298
311
  if i.tag == "p":
299
- if i.get("{http://www.w3.org/XML/1998/namespace}lang") is not None:
312
+ if i.get(_XML_LANG) is not None:
300
313
  paragraph_list.append(i)
301
314
  else:
302
315
  if len(paragraph_list) != 0:
@@ -321,7 +334,7 @@ class Description:
321
334
  paragraph_item.load_tags(paragraph_list)
322
335
  self.items.append(paragraph_item)
323
336
 
324
- def get_tags(self, parent_tag: etree.Element) -> None:
337
+ def get_tags(self, parent_tag: etree._Element) -> None:
325
338
  "Writes a Description tag. Only for internal use."
326
339
  description_tag = etree.SubElement(parent_tag, "description")
327
340
  for i in self.items:
@@ -66,6 +66,12 @@ PROVIDES_TYPES_LITERAL = Literal[
66
66
  "id"
67
67
  ]
68
68
 
69
+ RELATION_LITERAL = Literal[
70
+ "requires",
71
+ "recommends",
72
+ "supports"
73
+ ]
74
+
69
75
  RELATION_COMPARISON_OPERATOR_LITERAL = Literal[
70
76
  "eq",
71
77
  "ne",
@@ -88,11 +94,16 @@ CONTROL_TYPES_LITERAL = Literal[
88
94
  ]
89
95
 
90
96
  INTERNET_RELATION_VALUE_LITERAL = Literal[
91
- "always"
92
- "offline-only"
97
+ "always",
98
+ "offline-only",
93
99
  "first-run"
94
100
  ]
95
101
 
102
+ IMAGE_TYPE_LITERAL = Literal[
103
+ "source",
104
+ "thumbnail"
105
+ ]
106
+
96
107
  URL_TYPES = list(get_args(URL_TYPES_LITERAL))
97
108
  "All URL types"
98
109
 
@@ -108,11 +119,17 @@ OARS_VALUE_TYPES = list(get_args(OARS_VALUE_TYPES_LITERAL))
108
119
  PROVIDES_TYPES = list(get_args(PROVIDES_TYPES_LITERAL))
109
120
  "The list with all types for provides"
110
121
 
122
+ RELATION = list(get_args(RELATION_LITERAL))
123
+ "The list with all relations"
124
+
111
125
  RELATION_COMPARISON_OPERATOR = list(get_args(RELATION_COMPARISON_OPERATOR_LITERAL))
112
- "The aviable Operators for the relation compare attribute"
126
+ "The available Operators for the relation compare attribute"
113
127
 
114
128
  CONTROL_TYPES = list(get_args(CONTROL_TYPES_LITERAL))
115
129
  "The list with all possible values for control"
116
130
 
117
131
  INTERNET_RELATION_VALUE = list(get_args(INTERNET_RELATION_VALUE_LITERAL))
118
132
  "The list with all possible values for internet"
133
+
134
+ IMAGE_TYPE = list(get_args(IMAGE_TYPE_LITERAL))
135
+ "The list with all possible values for image type"
File without changes
@@ -0,0 +1 @@
1
+ 0.9.0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: appstream-python
3
- Version: 0.8
3
+ Version: 0.9.0
4
4
  Summary: A library for dealing with Freedesktop Appstream data
5
5
  Author-email: JakobDev <jakobdev@gmx.de>
6
- License: BSD-2-Clause
6
+ License-Expression: BSD-2-Clause
7
7
  Project-URL: Documentation, https://appstream-python.readthedocs.io
8
8
  Project-URL: Issues, https://codeberg.org/JakobDev/appstream-python/issues
9
9
  Project-URL: Source, https://codeberg.org/JakobDev/appstream-python
@@ -12,20 +12,22 @@ Keywords: JakobDev,AppStream,Freedesktop,Linux
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Environment :: Other Environment
15
- Classifier: License :: OSI Approved :: BSD License
16
15
  Classifier: Operating System :: POSIX
17
16
  Classifier: Operating System :: POSIX :: BSD
18
17
  Classifier: Operating System :: POSIX :: Linux
19
18
  Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.10
21
19
  Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
22
23
  Classifier: Programming Language :: Python :: 3 :: Only
23
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
- Requires-Python: >=3.10
25
+ Requires-Python: >=3.11
25
26
  Description-Content-Type: text/markdown
26
27
  License-File: LICENSE
27
- Requires-Dist: requests
28
28
  Requires-Dist: lxml
29
+ Requires-Dist: requests
30
+ Dynamic: license-file
29
31
 
30
32
  # appstream-python
31
33
 
@@ -9,6 +9,7 @@ appstream_python/Shared.py
9
9
  appstream_python/StandardConstants.py
10
10
  appstream_python/__init__.py
11
11
  appstream_python/_helper.py
12
+ appstream_python/py.typed
12
13
  appstream_python/version.txt
13
14
  appstream_python.egg-info/PKG-INFO
14
15
  appstream_python.egg-info/SOURCES.txt
@@ -16,6 +17,7 @@ appstream_python.egg-info/dependency_links.txt
16
17
  appstream_python.egg-info/requires.txt
17
18
  appstream_python.egg-info/top_level.txt
18
19
  tests/test_appstream_data.py
20
+ tests/test_collection.py
19
21
  tests/test_description.py
20
22
  tests/test_display_length.py
21
23
  tests/test_releases.py
@@ -6,9 +6,9 @@ build-backend = "setuptools.build_meta"
6
6
  name = "appstream-python"
7
7
  description = "A library for dealing with Freedesktop Appstream data"
8
8
  readme = "README.md"
9
- requires-python = ">=3.10"
9
+ requires-python = ">=3.11"
10
10
  keywords = ["JakobDev", "AppStream", "Freedesktop", "Linux"]
11
- license = { text = "BSD-2-Clause" }
11
+ license = "BSD-2-Clause"
12
12
  authors = [
13
13
  { name = "JakobDev", email = "jakobdev@gmx.de" }
14
14
  ]
@@ -16,19 +16,20 @@ classifiers = [
16
16
  "Development Status :: 3 - Alpha",
17
17
  "Intended Audience :: Developers",
18
18
  "Environment :: Other Environment",
19
- "License :: OSI Approved :: BSD License",
20
19
  "Operating System :: POSIX",
21
20
  "Operating System :: POSIX :: BSD",
22
21
  "Operating System :: POSIX :: Linux",
23
22
  "Programming Language :: Python :: 3",
24
- "Programming Language :: Python :: 3.10",
25
23
  "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
26
27
  "Programming Language :: Python :: 3 :: Only",
27
28
  "Programming Language :: Python :: Implementation :: CPython"
28
29
  ]
29
30
  dependencies = [
30
- "requests",
31
- "lxml"
31
+ "lxml",
32
+ "requests"
32
33
  ]
33
34
  dynamic = ["version"]
34
35
 
@@ -41,6 +42,9 @@ Donation = "https://ko-fi.com/jakobdev"
41
42
  [tool.setuptools.dynamic]
42
43
  version = { file = "appstream_python/version.txt" }
43
44
 
45
+ [tool.mypy]
46
+ modules = ["appstream_python"]
47
+
44
48
  [tool.pytest.ini_options]
45
49
  addopts = "-v --basetemp=pytest-temp --durations=10 --cov=appstream_python --cov-report html --cov-report term --cov-report term-missing"
46
50
  testpaths = ["tests"]
@@ -33,6 +33,8 @@ def assert_component_jdtextedit(component: appstream_python.AppstreamComponent)
33
33
  assert component.display_length["recommends"][0].px == 760
34
34
  assert component.display_length["recommends"][0].compare == "ge"
35
35
 
36
+ assert component.get_available_languages() == ["de"]
37
+
36
38
 
37
39
  def test_from_component_tag() -> None:
38
40
  root = etree.parse(JDTEXTEDIT_METAINFO)
@@ -0,0 +1,92 @@
1
+ import appstream_python
2
+ import pathlib
3
+
4
+
5
+ def _create_test_collection() -> appstream_python.AppstreamCollection:
6
+ collection = appstream_python.AppstreamCollection()
7
+
8
+ a_component = appstream_python.AppstreamComponent()
9
+ a_component.id = "com.example.A"
10
+ a_component.categories.append("Utility")
11
+ collection.add_component(a_component)
12
+
13
+ b_component = appstream_python.AppstreamComponent()
14
+ b_component.id = "com.example.B"
15
+ b_component.categories.append("Utility")
16
+ b_component.categories.append("Game")
17
+ collection.add_component(b_component)
18
+
19
+ c_component = appstream_python.AppstreamComponent()
20
+ c_component.id = "com.example.C"
21
+ c_component.categories.append("Game")
22
+ c_component.categories.append("System")
23
+ collection.add_component(c_component)
24
+
25
+ return collection
26
+
27
+
28
+ def test_collection_load_appstream_file() -> None:
29
+ collection = appstream_python.AppstreamCollection()
30
+ collection.load_appstream_file(pathlib.Path(__file__).parent / "data" / "AppStream" / "com.gitlab.JakobDev.jdTextEdit.metainfo.xml")
31
+ assert collection.get_component("com.gitlab.JakobDev.jdTextEdit").id == "com.gitlab.JakobDev.jdTextEdit"
32
+ assert len(collection) == 1
33
+
34
+
35
+ def test_collection_get_component_list() -> None:
36
+ component_list = _create_test_collection().get_component_list()
37
+ assert component_list[0].id == "com.example.A"
38
+ assert component_list[1].id == "com.example.B"
39
+ assert component_list[2].id == "com.example.C"
40
+ assert len(component_list) == 3
41
+
42
+
43
+ def test_collection_get_component_id_list() -> None:
44
+ assert _create_test_collection().get_component_id_list() == ["com.example.A", "com.example.B", "com.example.C"]
45
+
46
+
47
+ def test_collection_get_component() -> None:
48
+ collection = _create_test_collection()
49
+ assert collection.get_component("com.example.A").id == "com.example.A"
50
+ assert collection.get_component("com.example.B").id == "com.example.B"
51
+ assert collection.get_component("com.example.C").id == "com.example.C"
52
+ assert collection.get_component("com.example.D") is None
53
+
54
+
55
+ def test_collection_find_by_category() -> None:
56
+ collection = _create_test_collection()
57
+
58
+ utility_list = collection.find_by_category("Utility")
59
+ assert utility_list[0].id == "com.example.A"
60
+ assert utility_list[1].id == "com.example.B"
61
+ assert len(utility_list) == 2
62
+
63
+ game_list = collection.find_by_category("Game")
64
+ assert game_list[0].id == "com.example.B"
65
+ assert game_list[1].id == "com.example.C"
66
+ assert len(game_list) == 2
67
+
68
+ system_list = collection.find_by_category("System")
69
+ assert system_list[0].id == "com.example.C"
70
+ assert len(system_list) == 1
71
+
72
+ assert len(collection.find_by_category("Office")) == 0
73
+
74
+
75
+ def test_collection_write_load_uncompressed_collection(tmp_path: pathlib.Path) -> None:
76
+ old_collection = _create_test_collection()
77
+ old_collection.write_uncompressed_file(tmp_path / "test.xml")
78
+
79
+ new_collection = appstream_python.AppstreamCollection()
80
+ new_collection.load_uncompressed_appstream_collection(tmp_path / "test.xml")
81
+
82
+ assert old_collection.get_component_id_list() == new_collection.get_component_id_list()
83
+
84
+
85
+ def test_collection_write_load_compressed_collection(tmp_path: pathlib.Path) -> None:
86
+ old_collection = _create_test_collection()
87
+ old_collection.write_compressed_file(tmp_path / "test.xml.gz")
88
+
89
+ new_collection = appstream_python.AppstreamCollection()
90
+ new_collection.load_compressed_appstream_collection(tmp_path / "test.xml.gz")
91
+
92
+ assert old_collection.get_component_id_list() == new_collection.get_component_id_list()
@@ -1 +0,0 @@
1
- 0.8