appstream-python 0.6.3__py3-none-any.whl → 0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,10 @@
1
+ from .Shared import TranslateableTag, TranslateableList, Description
1
2
  from typing import Any, Optional, Literal, TypedDict, Union
3
+ from .Release import ReleaseList
2
4
  from ._helper import assert_func
3
5
  from .StandardConstants import *
4
6
  from lxml import etree
5
- import datetime
7
+ import dataclasses
6
8
  import io
7
9
  import os
8
10
 
@@ -35,358 +37,6 @@ class InternetRelationDict(TypedDict):
35
37
  bandwidth_mbitps: Optional[int]
36
38
 
37
39
 
38
- class TranslateableTag:
39
- "Represents a translatable tag"
40
-
41
- def __init__(self) -> None:
42
- self._text = ""
43
- self._translations: dict[str, str] = {}
44
-
45
- def get_default_text(self) -> str:
46
- """Returns the untranslated text"""
47
- return self._text
48
-
49
- def set_default_text(self, text: str) -> None:
50
- """Sets the untranslated text"""
51
- self._text = text
52
-
53
- def get_translated_text(self, lang: str) -> Optional[str]:
54
- """Returns the translated text"""
55
- return self._translations.get(lang, None)
56
-
57
- def get_translated_text_default(self, lang: str) -> Optional[str]:
58
- """Returns the translated text. Returns the default text, if the translation does not exists"""
59
- return self._translations.get(lang, self._text)
60
-
61
- def set_translated_text(self, lang: str, text: str) -> None:
62
- """Sets the translated text"""
63
- self._translations[lang] = text
64
-
65
- def get_available_languages(self) -> list[str]:
66
- """Returns a list with all languages of the tag"""
67
- return list(self._translations.keys())
68
-
69
- def load_tags(self, tag_list: list[etree.Element]) -> None:
70
- """Load a list of Tags"""
71
- for i in tag_list:
72
- if i.get("{http://www.w3.org/XML/1998/namespace}lang") is None:
73
- if i.text is not None:
74
- self._text = i.text.strip()
75
- else:
76
- self._text = ""
77
- else:
78
- if i.text is not None:
79
- self._translations[i.get(_XML_LANG)] = i.text.strip()
80
- else:
81
- self._translations[i.get(_XML_LANG)] = ""
82
-
83
- def write_tags(self, parent_tag: etree.Element, tag_name: str) -> None:
84
- """Writes a Tag"""
85
- default_tag = etree.SubElement(parent_tag, tag_name)
86
- default_tag.text = self._text
87
-
88
- for key, value in self._translations.items():
89
- translation_tag = etree.SubElement(parent_tag, tag_name)
90
- translation_tag.set("{http://www.w3.org/XML/1998/namespace}lang", key)
91
- translation_tag.text = value
92
-
93
- def clear(self) -> None:
94
- """Resets all data"""
95
- self._text = ""
96
- self._translations.clear()
97
-
98
- def __repr__(self) -> str:
99
- return f"<TranslateableTag default='{self._text}'>"
100
-
101
-
102
- class TranslateableList:
103
- "Represents a translatable list"
104
-
105
- def __init__(self) -> None:
106
- self._translated_data: dict[str, dict[str, str]] = {}
107
- self._translated_lists: dict[str, list[str]] = {}
108
-
109
- def get_default_list(self) -> list[str]:
110
- "Returns a list with the default items"
111
- return list(self._translated_data.keys())
112
-
113
- def get_translated_list(self, lang: str) -> list[str]:
114
- "Returns the translated list for the given language"
115
- if lang in self._translated_lists:
116
- return self._translated_lists[lang]
117
-
118
- return_list: list[str] = []
119
- for untranslated_text, translations in self._translated_data.items():
120
- return_list.append(translations.get(lang, untranslated_text))
121
- return return_list
122
-
123
- def load_tag(self, tag: etree.Element) -> None:
124
- "Loads an Tag. Only for internal use."
125
- if tag.get("{http://www.w3.org/XML/1998/namespace}lang") is None:
126
- current_text = ""
127
- for i in tag.getchildren():
128
- if i.get(_XML_LANG) is None:
129
- try:
130
- current_text = i.text.strip()
131
- self._translated_data[current_text] = {}
132
- except AttributeError:
133
- pass
134
- else:
135
- if current_text in self._translated_data:
136
- self._translated_data[current_text][i.get(_XML_LANG)] = i.text.strip()
137
- else:
138
- if tag.get("{http://www.w3.org/XML/1998/namespace}lang") not in self._translated_lists:
139
- self._translated_lists[tag.get("{http://www.w3.org/XML/1998/namespace}lang")] = []
140
- for i in tag.getchildren():
141
- self._translated_lists[tag.get("{http://www.w3.org/XML/1998/namespace}lang")].append(i.text.strip())
142
-
143
- def write_all_tag(self, parent_tag: etree.Element, tag_name: str) -> None:
144
- "Writes the XML tags. Onnly for internal use."
145
- for untranslated_text, translations in self._translated_data.items():
146
- default_tag = etree.SubElement(parent_tag, tag_name)
147
- default_tag.text = untranslated_text
148
- for lang, translated_text in translations.items():
149
- translated_tag = etree.SubElement(parent_tag, tag_name)
150
- translated_tag.set(_XML_LANG, lang)
151
- translated_tag.text = translated_text
152
-
153
- def write_untranslated_tags(self, parent_tag: etree.Element, tag_name: str) -> None:
154
- "Writes the untranslated XML tags. Onnly for internal use."
155
- for untranslated_text in self._translated_data.keys():
156
- default_tag = etree.SubElement(parent_tag, tag_name)
157
- default_tag.text = untranslated_text
158
-
159
- def write_translated_tags(self, parent_tag: etree.Element, tag_name: str, lang: str) -> None:
160
- "Writes the translated XML tags. Onnly for internal use."
161
- for untranslated_text, translations in self._translated_data.items():
162
- child_tag = etree.SubElement(parent_tag, tag_name)
163
- child_tag.text = translations.get(lang, untranslated_text)
164
-
165
- def clear(self) -> None:
166
- """Resets all data"""
167
- self._translated_data.clear()
168
-
169
-
170
- class DescriptionItem:
171
- "The Interface for a Description Item"
172
-
173
- def get_type(self) -> Literal["paragraph", "unordered-list", "ordered-list"]:
174
- "Retutns the Type of the Item"
175
- return "none"
176
-
177
- def load_tags(self, tag_list: list[etree.Element]) -> None:
178
- "Loads teh XML tags into the Elemnt. Only for internal use."
179
- raise NotImplementedError
180
-
181
- def get_tags(self, parent_tag: etree.Element) -> None:
182
- "Get the XML Tag from the Element. Only for internal use."
183
- raise NotImplementedError()
184
-
185
- def get_translated_tag(self, lang: Optional[str]) -> etree.Element:
186
- "Loads the tag for a given language"
187
- raise NotImplementedError()
188
-
189
- def to_plain_text(self, lang: Optional[str] = None) -> str:
190
- "Returns the content as plain text"
191
- raise NotImplementedError()
192
-
193
-
194
- class DescriptionParagraph(DescriptionItem):
195
- "Represents a paragraph <p> in the Description"
196
-
197
- def __init__(self) -> None:
198
- self.content = TranslateableTag()
199
- """The Text of the Paragraph"""
200
-
201
- def get_type(self) -> Literal["paragraph", "unordered-list", "ordered-list"]:
202
- "Retutns the Type of the Item"
203
- return "paragraph"
204
-
205
- def load_tags(self, tag_list: list[etree.Element]) -> None:
206
- "Loads teh XML tags into the Elemnt. Only for internal use."
207
- self.content.load_tags(tag_list)
208
-
209
- def get_tags(self, parent_tag: etree.Element) -> None:
210
- "Get the XML Tag from the Element. Only for internal use."
211
- self.content.write_tags(parent_tag, "p")
212
-
213
- def get_translated_tag(self, lang: Optional[str]) -> etree.Element:
214
- "Loads the tag for a given language"
215
- paragraph_tag = etree.Element("p")
216
- if lang is None:
217
- paragraph_tag.text = self.content.get_default_text()
218
- else:
219
- paragraph_tag.text = self.content.get_translated_text_default(lang)
220
- return paragraph_tag
221
-
222
- def to_plain_text(self, lang: Optional[str] = None) -> str:
223
- "Returns the content as plain text"
224
- return self.content.get_translated_text_default(lang).strip()
225
-
226
-
227
- class DescriptionList(DescriptionItem):
228
- "Represents a list <ul>/<ol> in the Description"
229
-
230
- def __init__(self, list_type: str) -> None:
231
- self._list_type = list_type
232
-
233
- self.content: TranslateableList = TranslateableList()
234
- "The list"
235
-
236
- def get_type(self) -> Literal["paragraph", "unordered-list", "ordered-list"]:
237
- "Retutns the Type of the Item"
238
- if self._list_type == "ul":
239
- return "unordered-list"
240
- else:
241
- return "ordered-list"
242
-
243
- def load_tags(self, tag_list: list[etree.Element]) -> None:
244
- "Loads the XML tags into the Elemnt. Only for internal use."
245
- self.content.load_tag(tag_list)
246
-
247
- def get_tags(self, parent_tag: etree.Element) -> None:
248
- "Get the XML Tag from the Element. Only for internal use."
249
- list_tag = etree.SubElement(parent_tag, self._list_type)
250
- self.content.write_all_tag(list_tag, "li")
251
-
252
- def get_translated_tag(self, lang: Optional[str] = None) -> etree.Element:
253
- "Loads the tag for a given language"
254
- list_tag = etree.Element(self._list_type)
255
- if lang is None:
256
- self.content.write_untranslated_tags(list_tag, "li")
257
- else:
258
- self.content.write_translated_tags(list_tag, "li", lang)
259
- return list_tag
260
-
261
- def to_plain_text(self, lang: Optional[str] = None) -> str:
262
- "Returns the content as plain text"
263
- tag_list = self.content.get_translated_list(lang)
264
-
265
- return_text = ""
266
- if self.get_type() == "unordered-list":
267
- for tag_text in tag_list:
268
- return_text += f"• {tag_text}\n"
269
- elif self.get_type() == "ordered-list":
270
- for count, tag_text in enumerate(tag_list):
271
- return_text += f"{count + 1}. {tag_text}\n"
272
-
273
- return return_text.strip()
274
-
275
-
276
- class Description:
277
- "Represents a <description> tag"
278
-
279
- def __init__(self) -> None:
280
- self.items: list[DescriptionItem] = []
281
- """All Description Items"""
282
-
283
- def load_tags(self, tag: etree.Element) -> None:
284
- "Load a XML tag. Onyl for internal use."
285
- paragraph_list: list[etree.Element] = []
286
- for i in tag.getchildren():
287
- if i.tag == "p":
288
- if i.get("{http://www.w3.org/XML/1998/namespace}lang") is not None:
289
- paragraph_list.append(i)
290
- else:
291
- if len(paragraph_list) != 0:
292
- paragraph_item = DescriptionParagraph()
293
- paragraph_item.load_tags(paragraph_list)
294
- self.items.append(paragraph_item)
295
- paragraph_list.clear()
296
- paragraph_list.append(i)
297
- elif i.tag in ("ul", "ol"):
298
- if len(paragraph_list) != 0:
299
- paragraph_item = DescriptionParagraph()
300
- paragraph_item.load_tags(paragraph_list)
301
- self.items.append(paragraph_item)
302
- paragraph_list.clear()
303
-
304
- list_item = DescriptionList(i.tag)
305
- list_item.load_tags(i)
306
- self.items.append(list_item)
307
-
308
- if len(paragraph_list) != 0:
309
- paragraph_item = DescriptionParagraph()
310
- paragraph_item.load_tags(paragraph_list)
311
- self.items.append(paragraph_item)
312
-
313
- def get_tags(self, parent_tag: etree.Element) -> None:
314
- "Writes a Description tag. Only for internal use."
315
- description_tag = etree.SubElement(parent_tag, "description")
316
- for i in self.items:
317
- i.get_tags(description_tag)
318
-
319
- def to_html(self, lang: Optional[str] = None) -> str:
320
- "Get the HTML code of the description in the given language"
321
- description_tag = etree.Element("description")
322
-
323
- for i in self.items:
324
- description_tag.append(i.get_translated_tag(lang))
325
-
326
- text = etree.tostring(description_tag, pretty_print=True, encoding="utf-8").decode("utf-8")
327
-
328
- # Remove the description tag
329
- text = text.replace("<description>", "")
330
- text = text.replace("</description>", "")
331
- text = text.replace("<description/>", "")
332
-
333
- # Remove the 2 spaces at the start of each line, after we removed the description tag
334
- return_text = ""
335
- for line in text.splitlines():
336
- return_text += line.removeprefix(" ") + "\n"
337
-
338
- return return_text.strip()
339
-
340
- def to_plain_text(self, lang: Optional[str] = None) -> str:
341
- """
342
- Converts the Description into Plain Text
343
-
344
- :param lang: The language
345
- :return: The Description
346
- """
347
- text = ""
348
-
349
- for i in self.items:
350
- text += f"{i.to_plain_text(lang)}\n\n"
351
-
352
- text = text.removesuffix("\n\n")
353
-
354
- return text.strip()
355
-
356
-
357
- class Release:
358
- "Represents a <release> tag"
359
-
360
- def __init__(self) -> None:
361
- self.version: str = ""
362
- "The version"
363
-
364
- self.date: Optional[datetime.date]
365
- "The date"
366
-
367
- self.description = Description()
368
- "The description"
369
-
370
- def load_tag(self, tag: etree.Element) -> None:
371
- "Loads a release tag"
372
-
373
- self.version = tag.get("version") or ""
374
-
375
- try:
376
- self.date = datetime.date.fromisoformat(tag.get("date"))
377
- except Exception:
378
- pass
379
-
380
- try:
381
- self.date = datetime.date.fromtimestamp(int(tag.get("timestamp")))
382
- except Exception:
383
- pass
384
-
385
- description_tag = tag.find("description")
386
- if description_tag is not None:
387
- self.description.load_tags(description_tag)
388
-
389
-
390
40
  class Image:
391
41
  "Represents a <image> tag"
392
42
 
@@ -541,6 +191,44 @@ class DisplayLength:
541
191
  return False
542
192
 
543
193
 
194
+ @dataclasses.dataclass
195
+ class Developer:
196
+ "Represents a <developer> tag"
197
+
198
+ id: Optional[str] = None
199
+ name: TranslateableTag = dataclasses.field(default_factory=lambda: TranslateableTag())
200
+
201
+ def load_tag(self, tag: etree.Element) -> None:
202
+ "Loads the data from a XML tag"
203
+ self.id = tag.get("id")
204
+
205
+ self.name.load_tags(tag.findall("name"))
206
+
207
+ def get_tag(self) -> etree.Element:
208
+ """
209
+ Returns the XML Tag
210
+
211
+ :return: The Tag
212
+ """
213
+ tag = etree.Element("developer")
214
+
215
+ if self.id is not None:
216
+ tag.set("id", self.id)
217
+
218
+ self.name.write_tags(tag, "name")
219
+
220
+ return tag
221
+
222
+ def clear(self) -> None:
223
+ """Resets all data"""
224
+ self.id = None
225
+ self.name.clear()
226
+
227
+ def is_empty(self) -> bool:
228
+ "Checks if the developer tag is empty"
229
+ return self.id is None and self.name.get_default_text() == ""
230
+
231
+
544
232
  class AppstreamComponent:
545
233
  "Represents AppStream Component"
546
234
 
@@ -554,8 +242,8 @@ class AppstreamComponent:
554
242
  self.name: TranslateableTag = TranslateableTag()
555
243
  "The component name"
556
244
 
557
- self.developer_name: TranslateableTag = TranslateableTag()
558
- "The developer name"
245
+ self.developer: Developer = Developer()
246
+ "The developer"
559
247
 
560
248
  self.summary: TranslateableTag = TranslateableTag()
561
249
  "The component summary"
@@ -572,6 +260,9 @@ class AppstreamComponent:
572
260
  self.urls: dict[URL_TYPES_LITERAL, str] = {}
573
261
  "The URLs"
574
262
 
263
+ self.launchables: dict[LAUNCHABLE_TYPES, str] = {}
264
+ "The launchables"
265
+
575
266
  self.oars: dict[OARS_ATTRIBUTE_TYPES_LITERAL, OARS_VALUE_TYPES_LITERAL] = {}
576
267
  "The content rating"
577
268
 
@@ -581,7 +272,7 @@ class AppstreamComponent:
581
272
  self.provides: dict[PROVIDES_TYPES_LITERAL, list[str]] = {}
582
273
  "The provides. The content of the depracted mimetype tag goes intp provides['mimetype']"
583
274
 
584
- self.releases: list[Release] = []
275
+ self.releases: ReleaseList = ReleaseList()
585
276
  "The releases"
586
277
 
587
278
  self.screenshots: list[Screenshot] = []
@@ -632,17 +323,18 @@ class AppstreamComponent:
632
323
  self.id = ""
633
324
  self.type = "desktop"
634
325
  self.name.clear()
635
- self.developer_name.clear()
326
+ self.developer.clear()
636
327
  self.summary.clear()
637
328
  self.description.items.clear()
638
329
  self.metadata_license = ""
639
330
  self.project_license = ""
640
331
  self.categories.clear()
641
332
  self.urls.clear()
333
+ self.launchables.clear()
642
334
  self.oars.clear()
643
335
  self.provides.clear()
644
- self.releases.clear()
645
- self.screenshots.clear
336
+ self.releases = ReleaseList()
337
+ self.screenshots.clear()
646
338
  self.project_group = None
647
339
  self.translation.clear()
648
340
  self.languages.clear()
@@ -704,7 +396,11 @@ class AppstreamComponent:
704
396
 
705
397
  self.name.load_tags(tag.findall("name"))
706
398
 
707
- self.developer_name.load_tags(tag.findall("developer_name"))
399
+ # For backward compatibility
400
+ self.developer.name.load_tags(tag.findall("developer_name"))
401
+
402
+ if (developer_tag := tag.find("developer")) is not None:
403
+ self.developer.load_tag(developer_tag)
708
404
 
709
405
  self.summary.load_tags(tag.findall("summary"))
710
406
 
@@ -729,17 +425,16 @@ class AppstreamComponent:
729
425
  if i.get("type") in URL_TYPES:
730
426
  self.urls[i.get("type")] = i.text.strip()
731
427
 
428
+ for i in tag.findall("launchable"):
429
+ if i.get("type") in LAUNCHABLE_TYPES:
430
+ self.launchables[i.get("type")] = i.text.strip()
431
+
732
432
  oars_tag = tag.find("content_rating")
733
433
  if oars_tag is not None:
734
434
  for i in oars_tag.findall("content_attribute"):
735
435
  if i.get("id") in OARS_ATTRIBUTE_TYPES and i.text.strip() in OARS_VALUE_TYPES:
736
436
  self.oars[i.get("id")] = i.text.strip()
737
437
 
738
- categories_tag = tag.find("categories")
739
- if categories_tag is not None:
740
- for i in categories_tag.findall("category"):
741
- self.categories.append(i.text.strip())
742
-
743
438
  provides_tag = tag.find("provides")
744
439
  if provides_tag is not None:
745
440
  for i in provides_tag.getchildren():
@@ -754,10 +449,7 @@ class AppstreamComponent:
754
449
 
755
450
  releases_tag = tag.find("releases")
756
451
  if releases_tag is not None:
757
- for i in releases_tag.findall("release"):
758
- release_object = Release()
759
- release_object.load_tag(i)
760
- self.releases.append(release_object)
452
+ self.releases = ReleaseList.from_tag(releases_tag)
761
453
 
762
454
  screenshots_tag = tag.find("screenshots")
763
455
  if screenshots_tag is not None:
@@ -905,7 +597,8 @@ class AppstreamComponent:
905
597
 
906
598
  self.name.write_tags(tag, "name")
907
599
 
908
- self.developer_name.write_tags(tag, "developer_name")
600
+ if not self.developer.is_empty():
601
+ tag.append(self.developer.get_tag())
909
602
 
910
603
  self.summary.write_tags(tag, "summary")
911
604
 
@@ -923,6 +616,12 @@ class AppstreamComponent:
923
616
  url_tag.set("type", key)
924
617
  url_tag.text = value
925
618
 
619
+ for key, value in self.launchables.items():
620
+ if key in LAUNCHABLE_TYPES:
621
+ url_tag = etree.SubElement(tag, "launchable")
622
+ url_tag.set("type", key)
623
+ url_tag.text = value
624
+
926
625
  oars_tag = etree.SubElement(tag, "content_rating")
927
626
  oars_tag.set("type", "oars-1.1")
928
627
  for key, value in self.oars.items():
@@ -949,6 +648,9 @@ class AppstreamComponent:
949
648
  if len(provides_tag.getchildren()) == 0:
950
649
  tag.remove(provides_tag)
951
650
 
651
+ if len(self.releases) != 0:
652
+ tag.append(self.releases.get_tag())
653
+
952
654
  if self.project_group:
953
655
  project_group_tag = etree.SubElement(tag, "project_group")
954
656
  project_group_tag.text = self.project_group
@@ -0,0 +1,187 @@
1
+ from dataclasses import dataclass
2
+ from typing import Type, Literal
3
+ from ._helper import assert_func
4
+ from .Shared import Description
5
+ from lxml import etree
6
+ import collections
7
+ import datetime
8
+ import requests
9
+ import copy
10
+ import os
11
+
12
+
13
+ ReleaseType = Literal["stable", "development"]
14
+ ReleaseListType = Literal["embedded", "external"]
15
+ UrgencyType = Literal["unknown", "low", "medium", "high", "critical"]
16
+
17
+
18
+ @dataclass
19
+ class Release:
20
+ "Represents a <release> tag"
21
+
22
+ version: str = ""
23
+ type: ReleaseType = "stable"
24
+ urgency: UrgencyType = "unknown"
25
+ date: datetime.date | None = None
26
+ description = Description()
27
+ date_eol: datetime.date | None = None
28
+
29
+ def __post_init__(self) -> None:
30
+ self.description = copy.deepcopy(self.description)
31
+
32
+ def get_tag(self) -> etree.Element:
33
+ """
34
+ Returns the XML Tag
35
+
36
+ :return: The Tag
37
+ """
38
+ tag = etree.Element("release")
39
+ tag.set("type", self.type)
40
+ tag.set("version", self.version)
41
+
42
+ if self.urgency != "unknown":
43
+ tag.set("urgency", self.urgency)
44
+
45
+ if self.date is not None:
46
+ tag.set("date", self.date.isoformat())
47
+
48
+ if self.date_eol is not None:
49
+ tag.set("date_eol", self.date_eol.isoformat())
50
+
51
+ self.description.get_tags(tag)
52
+
53
+ return tag
54
+
55
+ @classmethod
56
+ def from_tag(cls: Type["Release"], tag: etree._Element) -> "Release":
57
+ "Loads a release tag"
58
+ release = cls()
59
+
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
78
+
79
+ description_tag = tag.find("description")
80
+ if description_tag is not None:
81
+ release.description.load_tags(description_tag)
82
+
83
+ return release
84
+
85
+
86
+ class ReleaseList(collections.UserList[Release]):
87
+ "Represents a list of releases"
88
+
89
+ def __init__(self) -> None:
90
+ super().__init__()
91
+
92
+ self.type: ReleaseListType = "embedded"
93
+ "The type"
94
+
95
+ self.url: str = ""
96
+ "The URL if external"
97
+
98
+ def load_external_releases(self) -> None:
99
+ "Loads the external releases from the Internet"
100
+ if self.type != "external":
101
+ return
102
+
103
+ r = requests.get(self.url)
104
+
105
+ tag = etree.fromstring(r.content)
106
+
107
+ for single_release in tag.findall("release"):
108
+ self.append(Release.from_tag(single_release))
109
+
110
+ def get_tag(self) -> etree.Element:
111
+ """
112
+ Returns the XML Tag
113
+
114
+ :return: The Tag
115
+ """
116
+ tag = etree.Element("releases")
117
+
118
+ if self.type != "external":
119
+ for release in self.data:
120
+ tag.append(release.get_tag())
121
+ else:
122
+ tag.set("type", self.type)
123
+
124
+ if self.url != "":
125
+ tag.set("url", self.url)
126
+
127
+ return tag
128
+
129
+ def get_xml_string(self) -> str:
130
+ """
131
+ Returns the XML data of the ReleaseList as string
132
+
133
+ :return: The XMl as string
134
+ """
135
+ return etree.tostring(self.get_tag(), pretty_print=True, encoding=str).strip()
136
+
137
+ def save_file(self, path: str | os.PathLike) -> None:
138
+ """Saves the Component as XML file"""
139
+ with open(path, "wb") as f:
140
+ f.write(etree.tostring(self.get_tag(), pretty_print=True, xml_declaration=True, encoding="utf-8"))
141
+
142
+ @classmethod
143
+ def from_tag(cls: Type["ReleaseList"], tag: etree._Element, fetch_external: bool = False) -> "ReleaseList":
144
+ "Creates the list from an XMl tag"
145
+ release_list = cls()
146
+
147
+ release_list.type = tag.get("type", "embedded")
148
+ release_list.url = tag.get("url", "")
149
+
150
+ for single_release in tag.findall("release"):
151
+ release_list.append(Release.from_tag(single_release))
152
+
153
+ if fetch_external:
154
+ release_list.load_external_releases()
155
+
156
+ return release_list
157
+
158
+ @classmethod
159
+ def from_string(cls: Type["ReleaseList"], text: str) -> "ReleaseList":
160
+ "Loads the Releases from a string"
161
+ return cls.from_tag(etree.fromstring(text.encode("utf-8")))
162
+
163
+ @classmethod
164
+ def from_file(cls: Type["ReleaseList"], path: str | os.PathLike) -> "ReleaseList":
165
+ "Loads the Releases from a file"
166
+ with open(path, "r", encoding="utf-8") as f:
167
+ return cls.from_string(f.read())
168
+
169
+ @classmethod
170
+ def from_url(cls: Type["ReleaseList"], url: str) -> "ReleaseList":
171
+ "Loads the Releases from a URL"
172
+ return cls.from_tag(etree.fromstring(requests.get(url).content))
173
+
174
+ def __repr__(self) -> str:
175
+ return f"ReleaseList(type='{self.type}', url='{self.url}', data={self.data})"
176
+
177
+ def __eq__(self, obj: object) -> bool:
178
+ if not isinstance(obj, ReleaseList):
179
+ return False
180
+
181
+ try:
182
+ assert_func(self.url == obj.url)
183
+ assert_func(self.type == obj.type)
184
+ assert_func(self.data == obj.data)
185
+ return True
186
+ except AssertionError:
187
+ return False
@@ -0,0 +1,371 @@
1
+ from typing import Optional, Literal
2
+ from ._helper import assert_func
3
+ from lxml import etree
4
+
5
+
6
+ _XML_LANG = "{http://www.w3.org/XML/1998/namespace}lang"
7
+
8
+
9
+ class TranslateableTag:
10
+ "Represents a translatable tag"
11
+ def __init__(self) -> None:
12
+ self._text = ""
13
+ self._translations: dict[str, str] = {}
14
+
15
+ def get_default_text(self) -> str:
16
+ """Returns the untranslated text"""
17
+ return self._text
18
+
19
+ def set_default_text(self, text: str) -> None:
20
+ """Sets the untranslated text"""
21
+ self._text = text
22
+
23
+ def get_translated_text(self, lang: str) -> Optional[str]:
24
+ """Returns the translated text"""
25
+ return self._translations.get(lang, None)
26
+
27
+ def get_translated_text_default(self, lang: str) -> Optional[str]:
28
+ """Returns the translated text. Returns the default text, if the translation does not exists"""
29
+ return self._translations.get(lang, self._text)
30
+
31
+ def set_translated_text(self, lang: str, text: str) -> None:
32
+ """Sets the translated text"""
33
+ self._translations[lang] = text
34
+
35
+ def get_available_languages(self) -> list[str]:
36
+ """Returns a list with all languages of the tag"""
37
+ return list(self._translations.keys())
38
+
39
+ def load_tags(self, tag_list: list[etree.Element]) -> None:
40
+ """Load a list of Tags"""
41
+ for i in tag_list:
42
+ if i.get("{http://www.w3.org/XML/1998/namespace}lang") is None:
43
+ if i.text is not None:
44
+ self._text = i.text.strip()
45
+ else:
46
+ self._text = ""
47
+ else:
48
+ if i.text is not None:
49
+ self._translations[i.get(_XML_LANG)] = i.text.strip()
50
+ else:
51
+ self._translations[i.get(_XML_LANG)] = ""
52
+
53
+ def write_tags(self, parent_tag: etree.Element, tag_name: str) -> None:
54
+ """Writes a Tag"""
55
+ default_tag = etree.SubElement(parent_tag, tag_name)
56
+ default_tag.text = self._text
57
+
58
+ for key, value in self._translations.items():
59
+ translation_tag = etree.SubElement(parent_tag, tag_name)
60
+ translation_tag.set("{http://www.w3.org/XML/1998/namespace}lang", key)
61
+ translation_tag.text = value
62
+
63
+ def clear(self) -> None:
64
+ """Resets all data"""
65
+ self._text = ""
66
+ self._translations.clear()
67
+
68
+ def __repr__(self) -> str:
69
+ return f"<TranslateableTag default='{self._text}'>"
70
+
71
+ def __eq__(self, obj: object) -> bool:
72
+ if not isinstance(obj, TranslateableTag):
73
+ return False
74
+
75
+ try:
76
+ assert_func(self._translations == obj._translations)
77
+ return True
78
+ except AssertionError:
79
+ return False
80
+
81
+
82
+ class TranslateableList:
83
+ "Represents a translatable list"
84
+
85
+ def __init__(self) -> None:
86
+ self._translated_data: dict[str, dict[str, str]] = {}
87
+ self._translated_lists: dict[str, list[str]] = {}
88
+
89
+ def get_default_list(self) -> list[str]:
90
+ "Returns a list with the default items"
91
+ return list(self._translated_data.keys())
92
+
93
+ def get_translated_list(self, lang: str) -> list[str]:
94
+ "Returns the translated list for the given language"
95
+ if lang in self._translated_lists:
96
+ return self._translated_lists[lang]
97
+
98
+ return_list: list[str] = []
99
+ for untranslated_text, translations in self._translated_data.items():
100
+ return_list.append(translations.get(lang, untranslated_text))
101
+ return return_list
102
+
103
+ def load_tag(self, tag: etree.Element) -> None:
104
+ "Loads an Tag. Only for internal use."
105
+ if tag.get("{http://www.w3.org/XML/1998/namespace}lang") is None:
106
+ 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
114
+ else:
115
+ if current_text in self._translated_data:
116
+ self._translated_data[current_text][i.get(_XML_LANG)] = i.text.strip()
117
+ 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:
124
+ "Writes the XML tags. Onnly for internal use."
125
+ for untranslated_text, translations in self._translated_data.items():
126
+ default_tag = etree.SubElement(parent_tag, tag_name)
127
+ default_tag.text = untranslated_text
128
+ for lang, translated_text in translations.items():
129
+ translated_tag = etree.SubElement(parent_tag, tag_name)
130
+ translated_tag.set(_XML_LANG, lang)
131
+ translated_tag.text = translated_text
132
+
133
+ def write_untranslated_tags(self, parent_tag: etree.Element, tag_name: str) -> None:
134
+ "Writes the untranslated XML tags. Onnly for internal use."
135
+ for untranslated_text in self._translated_data.keys():
136
+ default_tag = etree.SubElement(parent_tag, tag_name)
137
+ default_tag.text = untranslated_text
138
+
139
+ def write_translated_tags(self, parent_tag: etree.Element, tag_name: str, lang: str) -> None:
140
+ "Writes the translated XML tags. Onnly for internal use."
141
+ for untranslated_text, translations in self._translated_data.items():
142
+ child_tag = etree.SubElement(parent_tag, tag_name)
143
+ child_tag.text = translations.get(lang, untranslated_text)
144
+
145
+ def clear(self) -> None:
146
+ """Resets all data"""
147
+ self._translated_data.clear()
148
+
149
+ def __eq__(self, obj: object) -> bool:
150
+ if not isinstance(obj, TranslateableList):
151
+ return False
152
+
153
+ try:
154
+ assert_func(self._translated_data == obj._translated_data)
155
+ assert_func(self._translated_lists == obj._translated_lists)
156
+ return True
157
+ except AssertionError:
158
+ return False
159
+
160
+
161
+ class DescriptionItem:
162
+ "The Interface for a Description Item"
163
+
164
+ def get_type(self) -> Literal["paragraph", "unordered-list", "ordered-list"]:
165
+ "Retutns the Type of the Item"
166
+ return "none"
167
+
168
+ def load_tags(self, tag_list: list[etree.Element]) -> None:
169
+ "Loads teh XML tags into the Elemnt. Only for internal use."
170
+ raise NotImplementedError
171
+
172
+ def get_tags(self, parent_tag: etree.Element) -> None:
173
+ "Get the XML Tag from the Element. Only for internal use."
174
+ raise NotImplementedError()
175
+
176
+ def get_translated_tag(self, lang: Optional[str]) -> etree.Element:
177
+ "Loads the tag for a given language"
178
+ raise NotImplementedError()
179
+
180
+ def to_plain_text(self, lang: Optional[str] = None) -> str:
181
+ "Returns the content as plain text"
182
+ raise NotImplementedError()
183
+
184
+
185
+ class DescriptionParagraph(DescriptionItem):
186
+ "Represents a paragraph <p> in the Description"
187
+
188
+ def __init__(self) -> None:
189
+ self.content = TranslateableTag()
190
+ """The Text of the Paragraph"""
191
+
192
+ def get_type(self) -> Literal["paragraph", "unordered-list", "ordered-list"]:
193
+ "Retutns the Type of the Item"
194
+ return "paragraph"
195
+
196
+ def load_tags(self, tag_list: list[etree.Element]) -> None:
197
+ "Loads teh XML tags into the Elemnt. Only for internal use."
198
+ self.content.load_tags(tag_list)
199
+
200
+ def get_tags(self, parent_tag: etree.Element) -> None:
201
+ "Get the XML Tag from the Element. Only for internal use."
202
+ self.content.write_tags(parent_tag, "p")
203
+
204
+ def get_translated_tag(self, lang: Optional[str]) -> etree.Element:
205
+ "Loads the tag for a given language"
206
+ paragraph_tag = etree.Element("p")
207
+ if lang is None:
208
+ paragraph_tag.text = self.content.get_default_text()
209
+ else:
210
+ paragraph_tag.text = self.content.get_translated_text_default(lang)
211
+ return paragraph_tag
212
+
213
+ def to_plain_text(self, lang: Optional[str] = None) -> str:
214
+ "Returns the content as plain text"
215
+ return self.content.get_translated_text_default(lang).strip()
216
+
217
+ def __repr__(self) -> str:
218
+ return f"<DescriptionParagraph default='{self.content.get_default_text()}'>"
219
+
220
+ def __eq__(self, obj: object) -> bool:
221
+ if not isinstance(obj, DescriptionParagraph):
222
+ return False
223
+
224
+ return self.content == obj.content
225
+
226
+
227
+ class DescriptionList(DescriptionItem):
228
+ "Represents a list <ul>/<ol> in the Description"
229
+
230
+ def __init__(self, list_type: str) -> None:
231
+ self._list_type = list_type
232
+
233
+ self.content: TranslateableList = TranslateableList()
234
+ "The list"
235
+
236
+ def get_type(self) -> Literal["paragraph", "unordered-list", "ordered-list"]:
237
+ "Returns the Type of the Item"
238
+ if self._list_type == "ul":
239
+ return "unordered-list"
240
+ else:
241
+ return "ordered-list"
242
+
243
+ def load_tags(self, tag_list: list[etree.Element]) -> None:
244
+ "Loads the XML tags into the Elemnt. Only for internal use."
245
+ self.content.load_tag(tag_list)
246
+
247
+ def get_tags(self, parent_tag: etree.Element) -> None:
248
+ "Get the XML Tag from the Element. Only for internal use."
249
+ list_tag = etree.SubElement(parent_tag, self._list_type)
250
+ self.content.write_all_tag(list_tag, "li")
251
+
252
+ def get_translated_tag(self, lang: Optional[str] = None) -> etree.Element:
253
+ "Loads the tag for a given language"
254
+ list_tag = etree.Element(self._list_type)
255
+ if lang is None:
256
+ self.content.write_untranslated_tags(list_tag, "li")
257
+ else:
258
+ self.content.write_translated_tags(list_tag, "li", lang)
259
+ return list_tag
260
+
261
+ def to_plain_text(self, lang: Optional[str] = None) -> str:
262
+ "Returns the content as plain text"
263
+ tag_list = self.content.get_translated_list(lang)
264
+
265
+ return_text = ""
266
+ if self.get_type() == "unordered-list":
267
+ for tag_text in tag_list:
268
+ return_text += f"• {tag_text}\n"
269
+ elif self.get_type() == "ordered-list":
270
+ for count, tag_text in enumerate(tag_list):
271
+ return_text += f"{count + 1}. {tag_text}\n"
272
+
273
+ return return_text.strip()
274
+
275
+ def __eq__(self, obj: object) -> bool:
276
+ if not isinstance(obj, DescriptionList):
277
+ return False
278
+
279
+ try:
280
+ assert_func(self._list_type == obj._list_type)
281
+ assert_func(self.content == obj.content)
282
+ return True
283
+ except AssertionError:
284
+ return False
285
+
286
+
287
+ class Description:
288
+ "Represents a <description> tag"
289
+
290
+ def __init__(self) -> None:
291
+ self.items: list[DescriptionItem] = []
292
+ """All Description Items"""
293
+
294
+ def load_tags(self, tag: etree.Element) -> None:
295
+ "Load a XML tag. Onyl for internal use."
296
+ paragraph_list: list[etree.Element] = []
297
+ for i in tag.getchildren():
298
+ if i.tag == "p":
299
+ if i.get("{http://www.w3.org/XML/1998/namespace}lang") is not None:
300
+ paragraph_list.append(i)
301
+ else:
302
+ if len(paragraph_list) != 0:
303
+ paragraph_item = DescriptionParagraph()
304
+ paragraph_item.load_tags(paragraph_list)
305
+ self.items.append(paragraph_item)
306
+ paragraph_list.clear()
307
+ paragraph_list.append(i)
308
+ elif i.tag in ("ul", "ol"):
309
+ if len(paragraph_list) != 0:
310
+ paragraph_item = DescriptionParagraph()
311
+ paragraph_item.load_tags(paragraph_list)
312
+ self.items.append(paragraph_item)
313
+ paragraph_list.clear()
314
+
315
+ list_item = DescriptionList(i.tag)
316
+ list_item.load_tags(i)
317
+ self.items.append(list_item)
318
+
319
+ if len(paragraph_list) != 0:
320
+ paragraph_item = DescriptionParagraph()
321
+ paragraph_item.load_tags(paragraph_list)
322
+ self.items.append(paragraph_item)
323
+
324
+ def get_tags(self, parent_tag: etree.Element) -> None:
325
+ "Writes a Description tag. Only for internal use."
326
+ description_tag = etree.SubElement(parent_tag, "description")
327
+ for i in self.items:
328
+ i.get_tags(description_tag)
329
+
330
+ def to_html(self, lang: Optional[str] = None) -> str:
331
+ "Get the HTML code of the description in the given language"
332
+ description_tag = etree.Element("description")
333
+
334
+ for i in self.items:
335
+ description_tag.append(i.get_translated_tag(lang))
336
+
337
+ text = etree.tostring(description_tag, pretty_print=True, encoding="utf-8").decode("utf-8")
338
+
339
+ # Remove the description tag
340
+ text = text.replace("<description>", "")
341
+ text = text.replace("</description>", "")
342
+ text = text.replace("<description/>", "")
343
+
344
+ # Remove the 2 spaces at the start of each line, after we removed the description tag
345
+ return_text = ""
346
+ for line in text.splitlines():
347
+ return_text += line.removeprefix(" ") + "\n"
348
+
349
+ return return_text.strip()
350
+
351
+ def to_plain_text(self, lang: Optional[str] = None) -> str:
352
+ """
353
+ Converts the Description into Plain Text
354
+
355
+ :param lang: The language
356
+ :return: The Description
357
+ """
358
+ text = ""
359
+
360
+ for i in self.items:
361
+ text += f"{i.to_plain_text(lang)}\n\n"
362
+
363
+ text = text.removesuffix("\n\n")
364
+
365
+ return text.strip()
366
+
367
+ def __eq__(self, obj: object) -> bool:
368
+ if not isinstance(obj, Description):
369
+ return False
370
+
371
+ return self.items == obj.items
@@ -13,6 +13,13 @@ URL_TYPES_LITERAL = Literal[
13
13
  "contribute"
14
14
  ]
15
15
 
16
+ LAUNCHABLE_TYPES_LITERAL = Literal[
17
+ "desktop-id",
18
+ "service",
19
+ "cockpit-manifest",
20
+ "url"
21
+ ]
22
+
16
23
  OARS_ATTRIBUTE_TYPES_LITERAL = Literal[
17
24
  "violence-cartoon",
18
25
  "violence-fantasy",
@@ -89,6 +96,9 @@ INTERNET_RELATION_VALUE_LITERAL = Literal[
89
96
  URL_TYPES = list(get_args(URL_TYPES_LITERAL))
90
97
  "All URL types"
91
98
 
99
+ LAUNCHABLE_TYPES = list(get_args(LAUNCHABLE_TYPES_LITERAL))
100
+ "All launchable types"
101
+
92
102
  OARS_ATTRIBUTE_TYPES = list(get_args(OARS_ATTRIBUTE_TYPES_LITERAL))
93
103
  "All aviable OARS attributes"
94
104
 
@@ -1,4 +1,5 @@
1
1
  from .Collection import AppstreamCollection
2
2
  from .Component import AppstreamComponent
3
+ from .Release import ReleaseList
3
4
 
4
- __all__ = ["AppstreamCollection", "AppstreamComponent", "StandardConstants"]
5
+ __all__ = ["AppstreamCollection", "AppstreamComponent", "ReleaseList", "StandardConstants"]
@@ -1 +1 @@
1
- 0.6.3
1
+ 0.8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appstream-python
3
- Version: 0.6.3
3
+ Version: 0.8
4
4
  Summary: A library for dealing with Freedesktop Appstream data
5
5
  Author-email: JakobDev <jakobdev@gmx.de>
6
6
  License: BSD-2-Clause
@@ -24,6 +24,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
24
24
  Requires-Python: >=3.10
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
+ Requires-Dist: requests
27
28
  Requires-Dist: lxml
28
29
 
29
30
  # appstream-python
@@ -0,0 +1,13 @@
1
+ appstream_python/Collection.py,sha256=D5Sx8M4YovOZ2OKkubgY0rxRA2IwhdXJqcFuAhhoIUE,2777
2
+ appstream_python/Component.py,sha256=BgFWQUv0XBsUZYp4WTuONXAmf80XE64Q7QG0x2PcD1U,24219
3
+ appstream_python/Release.py,sha256=sLQbv0T7FLYZYcfGivveVOKFD6rptoCAK0lbYCFYeiE,5409
4
+ appstream_python/Shared.py,sha256=zHeUDicNbuP_jXx3VTSNGuq7XFGUryZg0zMVCgGdXgo,13959
5
+ appstream_python/StandardConstants.py,sha256=-SzBKeXWFgY_6wc3SgptDlj88gOE_ztXBeBM3Xx3ko4,2376
6
+ appstream_python/__init__.py,sha256=eB0PvIoWrH0ZkNq2yY2h9xujdNqJMKmyM01KDpm6Yis,212
7
+ appstream_python/_helper.py,sha256=T1On8-taSPoKgpz9u8briuRO0uurcBL415o8g2Gc678,326
8
+ appstream_python/version.txt,sha256=_fJ28Bg8cXQoI7Bh9zQtWB5W3grxBkgzm1JSSeJu6U8,4
9
+ appstream_python-0.8.dist-info/LICENSE,sha256=KHWQLS2VDTyyF0CMgSrCU2j758vzhIUXA4viB-DWlTI,1318
10
+ appstream_python-0.8.dist-info/METADATA,sha256=6GzziBTwnxogHEFdDf_uxfUhsxG5EgiN-15nUJ7qOw4,1328
11
+ appstream_python-0.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
12
+ appstream_python-0.8.dist-info/top_level.txt,sha256=TEnvfaZcEAhAofLtB0lquKH1fwr9C-9dxpGN405dZiQ,17
13
+ appstream_python-0.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- appstream_python/Collection.py,sha256=D5Sx8M4YovOZ2OKkubgY0rxRA2IwhdXJqcFuAhhoIUE,2777
2
- appstream_python/Component.py,sha256=QrXSfWktl0Ki77m4jfySooSx6FgVVdfMFm-maJzQaGE,35969
3
- appstream_python/StandardConstants.py,sha256=xcpEJixO662W_vCuGV8RSByN70_0tXaM-ODHfNPhdwo,2186
4
- appstream_python/__init__.py,sha256=Bj7IpeFc_-HflTaV8I1LRxe1vIbBp2iZfXEJEtBE8Uw,164
5
- appstream_python/_helper.py,sha256=T1On8-taSPoKgpz9u8briuRO0uurcBL415o8g2Gc678,326
6
- appstream_python/version.txt,sha256=yfVuuTSMvINFva6nyiTJ-WDhWYrwVHJ5R2CiaC_weow,6
7
- appstream_python-0.6.3.dist-info/LICENSE,sha256=KHWQLS2VDTyyF0CMgSrCU2j758vzhIUXA4viB-DWlTI,1318
8
- appstream_python-0.6.3.dist-info/METADATA,sha256=h2XBYaJ01kE7PLNP80Auh6BltLpTalrzmv48VckNhuw,1306
9
- appstream_python-0.6.3.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
10
- appstream_python-0.6.3.dist-info/top_level.txt,sha256=TEnvfaZcEAhAofLtB0lquKH1fwr9C-9dxpGN405dZiQ,17
11
- appstream_python-0.6.3.dist-info/RECORD,,