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.
- appstream_python/Component.py +72 -370
- appstream_python/Release.py +187 -0
- appstream_python/Shared.py +371 -0
- appstream_python/StandardConstants.py +10 -0
- appstream_python/__init__.py +2 -1
- appstream_python/version.txt +1 -1
- {appstream_python-0.6.3.dist-info → appstream_python-0.8.dist-info}/METADATA +2 -1
- appstream_python-0.8.dist-info/RECORD +13 -0
- {appstream_python-0.6.3.dist-info → appstream_python-0.8.dist-info}/WHEEL +1 -1
- appstream_python-0.6.3.dist-info/RECORD +0 -11
- {appstream_python-0.6.3.dist-info → appstream_python-0.8.dist-info}/LICENSE +0 -0
- {appstream_python-0.6.3.dist-info → appstream_python-0.8.dist-info}/top_level.txt +0 -0
appstream_python/Component.py
CHANGED
|
@@ -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
|
|
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.
|
|
558
|
-
"The developer
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
appstream_python/__init__.py
CHANGED
|
@@ -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"]
|
appstream_python/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.8
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: appstream-python
|
|
3
|
-
Version: 0.
|
|
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,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,,
|
|
File without changes
|
|
File without changes
|