pcf-toolkit 0.2.5__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.
pcf_toolkit/xml.py ADDED
@@ -0,0 +1,484 @@
1
+ """XML serialization for PCF manifest models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import xml.etree.ElementTree as ET
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass
8
+
9
+ from pcf_toolkit import models
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ManifestXmlSerializer:
14
+ """Serializes manifest models into deterministic XML.
15
+
16
+ Attributes:
17
+ indent: String used for indentation in XML output.
18
+ xml_declaration: Whether to include XML declaration header.
19
+ encoding: Character encoding for XML output.
20
+ """
21
+
22
+ indent: str = " "
23
+ xml_declaration: bool = True
24
+ encoding: str = "utf-8"
25
+
26
+ def to_string(self, manifest: models.Manifest) -> str:
27
+ """Serializes a manifest model to XML string.
28
+
29
+ Args:
30
+ manifest: The Manifest model instance to serialize.
31
+
32
+ Returns:
33
+ XML string representation of the manifest.
34
+ """
35
+ root = self._manifest_to_element(manifest)
36
+ tree = ET.ElementTree(root)
37
+ ET.indent(tree, space=self.indent)
38
+ data = ET.tostring(root, encoding=self.encoding, xml_declaration=self.xml_declaration)
39
+ return data.decode(self.encoding)
40
+
41
+ def _manifest_to_element(self, manifest: models.Manifest) -> ET.Element:
42
+ """Converts a Manifest model to XML element.
43
+
44
+ Args:
45
+ manifest: The Manifest model to convert.
46
+
47
+ Returns:
48
+ Root XML element representing the manifest.
49
+ """
50
+ root = ET.Element("manifest")
51
+ root.append(self._control_to_element(manifest.control))
52
+ return root
53
+
54
+ def _control_to_element(self, control: models.Control) -> ET.Element:
55
+ """Converts a Control model to XML element.
56
+
57
+ Args:
58
+ control: The Control model to convert.
59
+
60
+ Returns:
61
+ XML element representing the control.
62
+ """
63
+ attribs = self._ordered_attribs(
64
+ [
65
+ ("namespace", control.namespace),
66
+ ("constructor", control.constructor),
67
+ ("version", control.version),
68
+ ("display-name-key", control.display_name_key),
69
+ ("description-key", control.description_key),
70
+ ("control-type", control.control_type.value if control.control_type else None),
71
+ ("preview-image", control.preview_image),
72
+ ]
73
+ )
74
+ element = ET.Element("control", attrib=attribs)
75
+ for item in control.property:
76
+ element.append(self._property_to_element(item))
77
+ for item in control.event:
78
+ element.append(self._event_to_element(item))
79
+ for item in control.data_set:
80
+ element.append(self._dataset_to_element(item))
81
+ for item in control.type_group:
82
+ element.append(self._type_group_to_element(item))
83
+ if control.property_dependencies:
84
+ element.append(self._property_dependencies_to_element(control.property_dependencies))
85
+ if control.feature_usage:
86
+ element.append(self._feature_usage_to_element(control.feature_usage))
87
+ if control.external_service_usage:
88
+ element.append(self._external_service_usage_to_element(control.external_service_usage))
89
+ if control.platform_action:
90
+ element.append(self._platform_action_to_element(control.platform_action))
91
+ element.append(self._resources_to_element(control.resources))
92
+ return element
93
+
94
+ def _property_to_element(self, prop: models.Property) -> ET.Element:
95
+ """Converts a Property model to XML element.
96
+
97
+ Args:
98
+ prop: The Property model to convert.
99
+
100
+ Returns:
101
+ XML element representing the property.
102
+ """
103
+ attribs = self._ordered_attribs(
104
+ [
105
+ ("name", prop.name),
106
+ ("display-name-key", prop.display_name_key),
107
+ ("description-key", prop.description_key),
108
+ ("of-type", prop.of_type.value if prop.of_type else None),
109
+ ("of-type-group", prop.of_type_group),
110
+ ("usage", prop.usage.value if prop.usage else None),
111
+ ("required", self._bool_value(prop.required)),
112
+ ("default-value", prop.default_value),
113
+ ("pfx-default-value", prop.pfx_default_value),
114
+ ]
115
+ )
116
+ element = ET.Element("property", attrib=attribs)
117
+ if prop.types:
118
+ element.append(self._types_to_element(prop.types))
119
+ for value in prop.values:
120
+ element.append(self._enum_value_to_element(value))
121
+ return element
122
+
123
+ def _event_to_element(self, event: models.Event) -> ET.Element:
124
+ """Converts an Event model to XML element.
125
+
126
+ Args:
127
+ event: The Event model to convert.
128
+
129
+ Returns:
130
+ XML element representing the event.
131
+ """
132
+ attribs = self._ordered_attribs(
133
+ [
134
+ ("name", event.name),
135
+ ("pfx-default-value", event.pfx_default_value),
136
+ ("display-name-key", event.display_name_key),
137
+ ("description-key", event.description_key),
138
+ ]
139
+ )
140
+ return ET.Element("event", attrib=attribs)
141
+
142
+ def _dataset_to_element(self, dataset: models.DataSet) -> ET.Element:
143
+ """Converts a DataSet model to XML element.
144
+
145
+ Args:
146
+ dataset: The DataSet model to convert.
147
+
148
+ Returns:
149
+ XML element representing the data set.
150
+ """
151
+ attribs = self._ordered_attribs(
152
+ [
153
+ ("name", dataset.name),
154
+ ("display-name-key", dataset.display_name_key),
155
+ ("description-key", dataset.description_key),
156
+ ("cds-data-set-options", dataset.cds_data_set_options),
157
+ ]
158
+ )
159
+ element = ET.Element("data-set", attrib=attribs)
160
+ for prop_set in dataset.property_set:
161
+ element.append(self._property_set_to_element(prop_set))
162
+ return element
163
+
164
+ def _property_set_to_element(self, prop_set: models.PropertySet) -> ET.Element:
165
+ """Converts a PropertySet model to XML element.
166
+
167
+ Args:
168
+ prop_set: The PropertySet model to convert.
169
+
170
+ Returns:
171
+ XML element representing the property set.
172
+ """
173
+ attribs = self._ordered_attribs(
174
+ [
175
+ ("name", prop_set.name),
176
+ ("display-name-key", prop_set.display_name_key),
177
+ ("description-key", prop_set.description_key),
178
+ ("of-type", prop_set.of_type.value if prop_set.of_type else None),
179
+ ("of-type-group", prop_set.of_type_group),
180
+ ("usage", prop_set.usage.value if prop_set.usage else None),
181
+ ("required", self._bool_value(prop_set.required)),
182
+ ]
183
+ )
184
+ element = ET.Element("property-set", attrib=attribs)
185
+ if prop_set.types:
186
+ element.append(self._types_to_element(prop_set.types))
187
+ return element
188
+
189
+ def _type_group_to_element(self, group: models.TypeGroup) -> ET.Element:
190
+ """Converts a TypeGroup model to XML element.
191
+
192
+ Args:
193
+ group: The TypeGroup model to convert.
194
+
195
+ Returns:
196
+ XML element representing the type group.
197
+ """
198
+ attribs = self._ordered_attribs([("name", group.name)])
199
+ element = ET.Element("type-group", attrib=attribs)
200
+ for item in group.types:
201
+ element.append(self._type_element(item))
202
+ return element
203
+
204
+ def _types_to_element(self, types_element: models.TypesElement) -> ET.Element:
205
+ """Converts a TypesElement model to XML element.
206
+
207
+ Args:
208
+ types_element: The TypesElement model to convert.
209
+
210
+ Returns:
211
+ XML element representing the types.
212
+ """
213
+ element = ET.Element("types")
214
+ for item in types_element.types:
215
+ element.append(self._type_element(item))
216
+ return element
217
+
218
+ def _type_element(self, type_element: models.TypeElement) -> ET.Element:
219
+ """Converts a TypeElement model to XML element.
220
+
221
+ Args:
222
+ type_element: The TypeElement model to convert.
223
+
224
+ Returns:
225
+ XML element representing the type.
226
+ """
227
+ element = ET.Element("type")
228
+ element.text = type_element.value.value
229
+ return element
230
+
231
+ def _enum_value_to_element(self, value: models.EnumValue) -> ET.Element:
232
+ """Converts an EnumValue model to XML element.
233
+
234
+ Args:
235
+ value: The EnumValue model to convert.
236
+
237
+ Returns:
238
+ XML element representing the enum value.
239
+ """
240
+ attribs = self._ordered_attribs(
241
+ [
242
+ ("name", value.name),
243
+ ("display-name-key", value.display_name_key),
244
+ ]
245
+ )
246
+ element = ET.Element("value", attrib=attribs)
247
+ element.text = str(value.value)
248
+ return element
249
+
250
+ def _property_dependencies_to_element(self, deps: models.PropertyDependencies) -> ET.Element:
251
+ """Converts PropertyDependencies model to XML element.
252
+
253
+ Args:
254
+ deps: The PropertyDependencies model to convert.
255
+
256
+ Returns:
257
+ XML element representing the property dependencies.
258
+ """
259
+ element = ET.Element("property-dependencies")
260
+ for dep in deps.property_dependency:
261
+ element.append(self._property_dependency_to_element(dep))
262
+ return element
263
+
264
+ def _property_dependency_to_element(self, dep: models.PropertyDependency) -> ET.Element:
265
+ """Converts a PropertyDependency model to XML element.
266
+
267
+ Args:
268
+ dep: The PropertyDependency model to convert.
269
+
270
+ Returns:
271
+ XML element representing the property dependency.
272
+ """
273
+ attribs = self._ordered_attribs(
274
+ [
275
+ ("input", dep.input),
276
+ ("output", dep.output),
277
+ ("required-for", dep.required_for.value),
278
+ ]
279
+ )
280
+ return ET.Element("property-dependency", attrib=attribs)
281
+
282
+ def _feature_usage_to_element(self, usage: models.FeatureUsage) -> ET.Element:
283
+ """Converts a FeatureUsage model to XML element.
284
+
285
+ Args:
286
+ usage: The FeatureUsage model to convert.
287
+
288
+ Returns:
289
+ XML element representing the feature usage.
290
+ """
291
+ element = ET.Element("feature-usage")
292
+ for item in usage.uses_feature:
293
+ element.append(self._uses_feature_to_element(item))
294
+ return element
295
+
296
+ def _uses_feature_to_element(self, feature: models.UsesFeature) -> ET.Element:
297
+ """Converts a UsesFeature model to XML element.
298
+
299
+ Args:
300
+ feature: The UsesFeature model to convert.
301
+
302
+ Returns:
303
+ XML element representing the uses-feature.
304
+ """
305
+ attribs = self._ordered_attribs(
306
+ [
307
+ ("name", feature.name),
308
+ ("required", self._bool_value(feature.required)),
309
+ ]
310
+ )
311
+ return ET.Element("uses-feature", attrib=attribs)
312
+
313
+ def _external_service_usage_to_element(self, usage: models.ExternalServiceUsage) -> ET.Element:
314
+ """Converts ExternalServiceUsage model to XML element.
315
+
316
+ Args:
317
+ usage: The ExternalServiceUsage model to convert.
318
+
319
+ Returns:
320
+ XML element representing the external service usage.
321
+ """
322
+ attribs = self._ordered_attribs([("enabled", self._bool_value(usage.enabled))])
323
+ element = ET.Element("external-service-usage", attrib=attribs)
324
+ for domain in usage.domain:
325
+ element.append(self._domain_to_element(domain))
326
+ return element
327
+
328
+ def _domain_to_element(self, domain: models.Domain) -> ET.Element:
329
+ """Converts a Domain model to XML element.
330
+
331
+ Args:
332
+ domain: The Domain model to convert.
333
+
334
+ Returns:
335
+ XML element representing the domain.
336
+ """
337
+ element = ET.Element("domain")
338
+ element.text = domain.value
339
+ return element
340
+
341
+ def _platform_action_to_element(self, action: models.PlatformAction) -> ET.Element:
342
+ """Converts a PlatformAction model to XML element.
343
+
344
+ Args:
345
+ action: The PlatformAction model to convert.
346
+
347
+ Returns:
348
+ XML element representing the platform action.
349
+ """
350
+ attribs = self._ordered_attribs([("action-type", action.action_type.value if action.action_type else None)])
351
+ return ET.Element("platform-action", attrib=attribs)
352
+
353
+ def _resources_to_element(self, resources: models.Resources) -> ET.Element:
354
+ """Converts a Resources model to XML element.
355
+
356
+ Args:
357
+ resources: The Resources model to convert.
358
+
359
+ Returns:
360
+ XML element representing the resources.
361
+ """
362
+ element = ET.Element("resources")
363
+ element.append(self._code_to_element(resources.code))
364
+ for item in resources.css:
365
+ element.append(self._css_to_element(item))
366
+ for item in resources.resx:
367
+ element.append(self._resx_to_element(item))
368
+ for item in resources.img:
369
+ element.append(self._img_to_element(item))
370
+ for item in resources.platform_library:
371
+ element.append(self._platform_library_to_element(item))
372
+ for item in resources.dependency:
373
+ element.append(self._dependency_to_element(item))
374
+ return element
375
+
376
+ def _code_to_element(self, code: models.Code) -> ET.Element:
377
+ """Converts a Code model to XML element.
378
+
379
+ Args:
380
+ code: The Code model to convert.
381
+
382
+ Returns:
383
+ XML element representing the code resource.
384
+ """
385
+ attribs = self._ordered_attribs([("path", code.path), ("order", str(code.order))])
386
+ return ET.Element("code", attrib=attribs)
387
+
388
+ def _css_to_element(self, css: models.Css) -> ET.Element:
389
+ """Converts a Css model to XML element.
390
+
391
+ Args:
392
+ css: The Css model to convert.
393
+
394
+ Returns:
395
+ XML element representing the CSS resource.
396
+ """
397
+ attribs = self._ordered_attribs([("path", css.path), ("order", str(css.order) if css.order else None)])
398
+ return ET.Element("css", attrib=attribs)
399
+
400
+ def _img_to_element(self, img: models.Img) -> ET.Element:
401
+ """Converts an Img model to XML element.
402
+
403
+ Args:
404
+ img: The Img model to convert.
405
+
406
+ Returns:
407
+ XML element representing the image resource.
408
+ """
409
+ attribs = self._ordered_attribs([("path", img.path)])
410
+ return ET.Element("img", attrib=attribs)
411
+
412
+ def _resx_to_element(self, resx: models.Resx) -> ET.Element:
413
+ """Converts a Resx model to XML element.
414
+
415
+ Args:
416
+ resx: The Resx model to convert.
417
+
418
+ Returns:
419
+ XML element representing the resx resource.
420
+ """
421
+ attribs = self._ordered_attribs([("path", resx.path), ("version", resx.version)])
422
+ return ET.Element("resx", attrib=attribs)
423
+
424
+ def _platform_library_to_element(self, library: models.PlatformLibrary) -> ET.Element:
425
+ """Converts a PlatformLibrary model to XML element.
426
+
427
+ Args:
428
+ library: The PlatformLibrary model to convert.
429
+
430
+ Returns:
431
+ XML element representing the platform library.
432
+ """
433
+ attribs = self._ordered_attribs([("name", library.name.value), ("version", library.version)])
434
+ return ET.Element("platform-library", attrib=attribs)
435
+
436
+ def _dependency_to_element(self, dependency: models.Dependency) -> ET.Element:
437
+ """Converts a Dependency model to XML element.
438
+
439
+ Args:
440
+ dependency: The Dependency model to convert.
441
+
442
+ Returns:
443
+ XML element representing the dependency.
444
+ """
445
+ attribs = self._ordered_attribs(
446
+ [
447
+ ("type", dependency.type.value),
448
+ ("name", dependency.name),
449
+ ("order", str(dependency.order) if dependency.order else None),
450
+ ("load-type", dependency.load_type.value if dependency.load_type else None),
451
+ ]
452
+ )
453
+ return ET.Element("dependency", attrib=attribs)
454
+
455
+ def _ordered_attribs(self, pairs: Iterable[tuple[str, str | None]]) -> dict[str, str]:
456
+ """Builds an ordered attributes dictionary from key-value pairs.
457
+
458
+ Filters out None and empty string values.
459
+
460
+ Args:
461
+ pairs: Iterable of (key, value) tuples.
462
+
463
+ Returns:
464
+ Dictionary of attributes with non-empty values.
465
+ """
466
+ attribs: dict[str, str] = {}
467
+ for key, value in pairs:
468
+ if value is None or value == "":
469
+ continue
470
+ attribs[key] = value
471
+ return attribs
472
+
473
+ def _bool_value(self, value: bool | None) -> str | None:
474
+ """Converts a boolean value to XML string representation.
475
+
476
+ Args:
477
+ value: Boolean value to convert, or None.
478
+
479
+ Returns:
480
+ "true" or "false" string, or None if value is None.
481
+ """
482
+ if value is None:
483
+ return None
484
+ return "true" if value else "false"