xtce-lib 0.1.0a1__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.
- xtce_lib/__init__.py +33 -0
- xtce_lib/common/__init__.py +1 -0
- xtce_lib/common/validation.py +71 -0
- xtce_lib/common/xtce_database.py +286 -0
- xtce_lib/common/xtce_file.py +207 -0
- xtce_lib/common/xtce_path.py +344 -0
- xtce_lib/common/xtce_registry.py +96 -0
- xtce_lib/common/xtce_version.py +45 -0
- xtce_lib/exceptions.py +44 -0
- xtce_lib/generated/__init__.py +5 -0
- xtce_lib/generated/xtce_1_0/__init__.py +249 -0
- xtce_lib/generated/xtce_1_0/models.py +5637 -0
- xtce_lib/generated/xtce_1_1/__init__.py +277 -0
- xtce_lib/generated/xtce_1_1/models.py +6265 -0
- xtce_lib/generated/xtce_1_2/__init__.py +633 -0
- xtce_lib/generated/xtce_1_2/models.py +9964 -0
- xtce_lib/generated/xtce_1_3/__init__.py +645 -0
- xtce_lib/generated/xtce_1_3/models.py +10570 -0
- xtce_lib/py.typed +0 -0
- xtce_lib/xsd/dtc-05-01-05.xsd +2798 -0
- xtce_lib/xsd/dtc-06-11-06.xsd +3229 -0
- xtce_lib/xsd/dtc-18-02-04.xsd +5918 -0
- xtce_lib/xsd/dtc-25-02-18.xsd +6363 -0
- xtce_lib/xsd/xml.xsd +117 -0
- xtce_lib/xtce/__init__.py +602 -0
- xtce_lib/xtce/_base.py +400 -0
- xtce_lib/xtce/_pattern.py +15 -0
- xtce_lib/xtce/_type_aliases.py +8 -0
- xtce_lib/xtce/_type_aliases_ext.py +69 -0
- xtce_lib/xtce/_util.py +157 -0
- xtce_lib/xtce/alarm.py +1710 -0
- xtce_lib/xtce/algorithm.py +1276 -0
- xtce_lib/xtce/argument.py +481 -0
- xtce_lib/xtce/array.py +231 -0
- xtce_lib/xtce/calibrator.py +1037 -0
- xtce_lib/xtce/codec.py +2064 -0
- xtce_lib/xtce/command.py +2188 -0
- xtce_lib/xtce/common.py +459 -0
- xtce_lib/xtce/condition.py +1497 -0
- xtce_lib/xtce/container.py +2717 -0
- xtce_lib/xtce/datatype.py +2695 -0
- xtce_lib/xtce/enum.py +1065 -0
- xtce_lib/xtce/parameter.py +1429 -0
- xtce_lib/xtce/range.py +556 -0
- xtce_lib/xtce/reference.py +725 -0
- xtce_lib/xtce/space_system.py +652 -0
- xtce_lib/xtce/stream.py +914 -0
- xtce_lib/xtce/telemetry.py +554 -0
- xtce_lib/xtce/time.py +424 -0
- xtce_lib/xtce/trigger.py +410 -0
- xtce_lib/xtce/verifier.py +807 -0
- xtce_lib-0.1.0a1.dist-info/METADATA +153 -0
- xtce_lib-0.1.0a1.dist-info/RECORD +55 -0
- xtce_lib-0.1.0a1.dist-info/WHEEL +4 -0
- xtce_lib-0.1.0a1.dist-info/licenses/LICENSE +21 -0
xtce_lib/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""xtce-lib."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
log.addHandler(logging.NullHandler())
|
|
7
|
+
|
|
8
|
+
from . import generated, xtce
|
|
9
|
+
from .common.validation import ValidationReport, XtceSchemaError, XtceSemanticError
|
|
10
|
+
from .common.xtce_database import XtceDatabase
|
|
11
|
+
from .common.xtce_file import XtceFile
|
|
12
|
+
from .common.xtce_path import PathNode, XtcePath
|
|
13
|
+
from .common.xtce_registry import ResolvedReference, XtceRegistry
|
|
14
|
+
from .common.xtce_version import XtceVersion
|
|
15
|
+
from .exceptions import DowngradePolicy, XtceDowngradeError, XtceUnsupportedError
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"generated",
|
|
19
|
+
"xtce",
|
|
20
|
+
"ValidationReport",
|
|
21
|
+
"XtceSchemaError",
|
|
22
|
+
"XtceSemanticError",
|
|
23
|
+
"XtceDatabase",
|
|
24
|
+
"XtceFile",
|
|
25
|
+
"PathNode",
|
|
26
|
+
"XtcePath",
|
|
27
|
+
"ResolvedReference",
|
|
28
|
+
"XtceRegistry",
|
|
29
|
+
"XtceVersion",
|
|
30
|
+
"DowngradePolicy",
|
|
31
|
+
"XtceDowngradeError",
|
|
32
|
+
"XtceUnsupportedError",
|
|
33
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""XTCE common module."""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Common validation utilities."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from .xtce_path import XtcePath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseValidationError(ABC):
|
|
11
|
+
"""Abstract base class for all validation errors."""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def format(self) -> str:
|
|
15
|
+
"""Return a formatted string representation of the error."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class XtceSchemaError(BaseValidationError):
|
|
21
|
+
"""An XTCE schema validation error."""
|
|
22
|
+
|
|
23
|
+
line: int
|
|
24
|
+
message: str
|
|
25
|
+
|
|
26
|
+
def format(self) -> str:
|
|
27
|
+
"""Return a formatted string representation of the error."""
|
|
28
|
+
return f"Line {self.line}: {self.message}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class XtceSemanticError(BaseValidationError):
|
|
33
|
+
"""An XTCE semantic validation error."""
|
|
34
|
+
|
|
35
|
+
scope: XtcePath
|
|
36
|
+
message: str
|
|
37
|
+
|
|
38
|
+
def format(self) -> str:
|
|
39
|
+
"""Return a formatted string representation of the error."""
|
|
40
|
+
return f"[{self.scope}] {self.message}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
ErrorType = TypeVar("ErrorType", bound=BaseValidationError)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ValidationReport(Generic[ErrorType]):
|
|
48
|
+
"""A generic report for XTCE validation."""
|
|
49
|
+
|
|
50
|
+
title: str
|
|
51
|
+
errors: list[ErrorType] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_valid(self) -> bool:
|
|
55
|
+
"""True if the validation pass found no errors."""
|
|
56
|
+
return len(self.errors) == 0
|
|
57
|
+
|
|
58
|
+
def add_error(self, error: ErrorType) -> None:
|
|
59
|
+
"""Append an error to the report."""
|
|
60
|
+
self.errors.append(error)
|
|
61
|
+
|
|
62
|
+
def summary(self) -> str:
|
|
63
|
+
"""Generate a console-friendly report."""
|
|
64
|
+
if self.is_valid:
|
|
65
|
+
return f"{self.title}: PASSED"
|
|
66
|
+
|
|
67
|
+
lines = [f"{self.title}: FAILED with {len(self.errors)} errors"]
|
|
68
|
+
for err in self.errors:
|
|
69
|
+
lines.append(f" {err.format()}")
|
|
70
|
+
|
|
71
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""XTCE database object."""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Iterable, Self
|
|
6
|
+
|
|
7
|
+
from pydantic import validate_call
|
|
8
|
+
from xsdata.formats.dataclass.serializers import XmlSerializer
|
|
9
|
+
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
|
10
|
+
|
|
11
|
+
from xtce_lib.common.xtce_version import XtceVersion
|
|
12
|
+
from xtce_lib.exceptions import DowngradePolicy
|
|
13
|
+
from xtce_lib.xtce._type_aliases_ext import ReferenceableXtceObject
|
|
14
|
+
from xtce_lib.xtce.command import CommandMetadata, MetaCommandRef
|
|
15
|
+
from xtce_lib.xtce.common import Alias, AncillaryData
|
|
16
|
+
from xtce_lib.xtce.enum import SystemType
|
|
17
|
+
from xtce_lib.xtce.reference import ParameterRef
|
|
18
|
+
from xtce_lib.xtce.space_system import Header, Service, SpaceSystem
|
|
19
|
+
from xtce_lib.xtce.telemetry import TelemetryMetadata
|
|
20
|
+
|
|
21
|
+
from .validation import ValidationReport, XtceSemanticError
|
|
22
|
+
from .xtce_file import XtceFile
|
|
23
|
+
from .xtce_path import XtcePath
|
|
24
|
+
from .xtce_registry import XtceRegistry
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class XtceDatabase:
|
|
28
|
+
"""An XTCE database."""
|
|
29
|
+
|
|
30
|
+
@validate_call
|
|
31
|
+
def __init__(self, name: str) -> None:
|
|
32
|
+
"""Initialize a new XTCE database."""
|
|
33
|
+
self._root_system = SpaceSystem(name=name)
|
|
34
|
+
self._registry: XtceRegistry | None = None
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
@validate_call
|
|
38
|
+
def from_space_system(cls, space_system: SpaceSystem) -> Self:
|
|
39
|
+
"""Create a new XTCE database from an existing SpaceSystem."""
|
|
40
|
+
# Bypass __init__
|
|
41
|
+
instance = cls.__new__(cls)
|
|
42
|
+
instance._root_system = space_system
|
|
43
|
+
instance._registry = None
|
|
44
|
+
return instance
|
|
45
|
+
|
|
46
|
+
# TODO maybe from_file(), not sure how to handle XtceFile vs XtceDatabase yet
|
|
47
|
+
|
|
48
|
+
# The below methods allow for accessing the properties of the root SpaceSystem
|
|
49
|
+
# directly from the database, makes for a slightly cleaner API
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def name(self) -> str:
|
|
53
|
+
"""The name of the SpaceSystem."""
|
|
54
|
+
return self._root_system.name
|
|
55
|
+
|
|
56
|
+
@name.setter
|
|
57
|
+
def name(self, value: str) -> None:
|
|
58
|
+
self._root_system.name = value
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def short_description(self) -> str | None:
|
|
62
|
+
"""The short description of the SpaceSystem."""
|
|
63
|
+
return self._root_system.short_description
|
|
64
|
+
|
|
65
|
+
@short_description.setter
|
|
66
|
+
def short_description(self, value: str | None) -> None:
|
|
67
|
+
self._root_system.short_description = value
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def long_description(self) -> str | None:
|
|
71
|
+
"""The long description of the SpaceSystem."""
|
|
72
|
+
return self._root_system.long_description
|
|
73
|
+
|
|
74
|
+
@long_description.setter
|
|
75
|
+
def long_description(self, value: str | None) -> None:
|
|
76
|
+
self._root_system.long_description = value
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def aliases(self) -> list[Alias]:
|
|
80
|
+
"""The aliases of the SpaceSystem."""
|
|
81
|
+
return self._root_system.aliases
|
|
82
|
+
|
|
83
|
+
@aliases.setter
|
|
84
|
+
def aliases(self, value: list[Alias]) -> None:
|
|
85
|
+
self._root_system.aliases = value
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def ancillary_data(self) -> list[AncillaryData]:
|
|
89
|
+
"""The ancillary data of the SpaceSystem."""
|
|
90
|
+
return self._root_system.ancillary_data
|
|
91
|
+
|
|
92
|
+
@ancillary_data.setter
|
|
93
|
+
def ancillary_data(self, value: list[AncillaryData]) -> None:
|
|
94
|
+
self._root_system.ancillary_data = value
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def header(self) -> Header | None:
|
|
98
|
+
"""The header of the SpaceSystem."""
|
|
99
|
+
return self._root_system.header
|
|
100
|
+
|
|
101
|
+
@header.setter
|
|
102
|
+
def header(self, value: Header | None) -> None:
|
|
103
|
+
self._root_system.header = value
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def telemetry_metadata(self) -> TelemetryMetadata | None:
|
|
107
|
+
"""The telemetry metadata of the SpaceSystem."""
|
|
108
|
+
return self._root_system.telemetry_metadata
|
|
109
|
+
|
|
110
|
+
@telemetry_metadata.setter
|
|
111
|
+
def telemetry_metadata(self, value: TelemetryMetadata | None) -> None:
|
|
112
|
+
self._root_system.telemetry_metadata = value
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def command_metadata(self) -> CommandMetadata | None:
|
|
116
|
+
"""The command metadata of the SpaceSystem."""
|
|
117
|
+
return self._root_system.command_metadata
|
|
118
|
+
|
|
119
|
+
@command_metadata.setter
|
|
120
|
+
def command_metadata(self, value: CommandMetadata | None) -> None:
|
|
121
|
+
self._root_system.command_metadata = value
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def services(self) -> list[Service]:
|
|
125
|
+
"""The services of the SpaceSystem."""
|
|
126
|
+
return self._root_system.services
|
|
127
|
+
|
|
128
|
+
@services.setter
|
|
129
|
+
def services(self, value: list[Service]) -> None:
|
|
130
|
+
self._root_system.services = value
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def space_systems(self) -> list[SpaceSystem]:
|
|
134
|
+
"""The child SpaceSystems of the SpaceSystem."""
|
|
135
|
+
return self._root_system.space_systems
|
|
136
|
+
|
|
137
|
+
@space_systems.setter
|
|
138
|
+
def space_systems(self, value: list[SpaceSystem]) -> None:
|
|
139
|
+
self._root_system.space_systems = value
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def system_type(self) -> SystemType:
|
|
143
|
+
"""The system type of the SpaceSystem."""
|
|
144
|
+
return self._root_system.system_type
|
|
145
|
+
|
|
146
|
+
@system_type.setter
|
|
147
|
+
def system_type(self, value: SystemType) -> None:
|
|
148
|
+
self._root_system.system_type = value
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def asset_type(self) -> str:
|
|
152
|
+
"""The asset type of the SpaceSystem."""
|
|
153
|
+
return self._root_system.asset_type
|
|
154
|
+
|
|
155
|
+
@asset_type.setter
|
|
156
|
+
def asset_type(self, value: str) -> None:
|
|
157
|
+
self._root_system.asset_type = value
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def operational_status(self) -> str | None:
|
|
161
|
+
"""The operational status of the SpaceSystem."""
|
|
162
|
+
return self._root_system.operational_status
|
|
163
|
+
|
|
164
|
+
@operational_status.setter
|
|
165
|
+
def operational_status(self, value: str | None) -> None:
|
|
166
|
+
self._root_system.operational_status = value
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def base(self) -> str | None:
|
|
170
|
+
"""The base of the SpaceSystem."""
|
|
171
|
+
return self._root_system.base
|
|
172
|
+
|
|
173
|
+
@base.setter
|
|
174
|
+
def base(self, value: str | None) -> None:
|
|
175
|
+
self._root_system.base = value
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def root_system(self) -> SpaceSystem:
|
|
179
|
+
"""The root SpaceSystem of the database."""
|
|
180
|
+
return self._root_system
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def registry(self) -> XtceRegistry:
|
|
184
|
+
"""Get the registry of all XTCE objects in this database."""
|
|
185
|
+
if self._registry is None:
|
|
186
|
+
self._registry = XtceRegistry()
|
|
187
|
+
self._index_space_system(self.root_system, XtcePath("/"), self._registry)
|
|
188
|
+
return self._registry
|
|
189
|
+
|
|
190
|
+
def rebuild_registry(self) -> None:
|
|
191
|
+
"""Force a rebuild of the registry."""
|
|
192
|
+
new_registry = XtceRegistry()
|
|
193
|
+
self._index_space_system(self.root_system, XtcePath("/"), new_registry)
|
|
194
|
+
self._registry = new_registry
|
|
195
|
+
|
|
196
|
+
def validate(self) -> ValidationReport[XtceSemanticError]:
|
|
197
|
+
"""Perform semantic validation of this database."""
|
|
198
|
+
report = ValidationReport[XtceSemanticError](title="Semantic Validation")
|
|
199
|
+
self.rebuild_registry() # Ensure registry is up to date before validation
|
|
200
|
+
self.root_system.validate_semantics(report, self.registry, XtcePath("/"))
|
|
201
|
+
return report
|
|
202
|
+
|
|
203
|
+
@validate_call
|
|
204
|
+
def to_file(
|
|
205
|
+
self,
|
|
206
|
+
path: Path,
|
|
207
|
+
xtce_version: XtceVersion,
|
|
208
|
+
*,
|
|
209
|
+
downgrade_policy: DowngradePolicy = DowngradePolicy.STRICT,
|
|
210
|
+
**kwargs: Any,
|
|
211
|
+
) -> XtceFile:
|
|
212
|
+
"""Write the database to an XTCE XML file.
|
|
213
|
+
|
|
214
|
+
Any keyword arguments override the default serializer configuration.
|
|
215
|
+
"""
|
|
216
|
+
# Translate the SpaceSystem
|
|
217
|
+
space_system = self._root_system.to_xsdata(
|
|
218
|
+
version=xtce_version,
|
|
219
|
+
policy=downgrade_policy,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Serialize to file
|
|
223
|
+
config_kwargs: dict[str, Any] = {
|
|
224
|
+
"indent": " ",
|
|
225
|
+
}
|
|
226
|
+
config_kwargs.update(kwargs)
|
|
227
|
+
config = SerializerConfig(**config_kwargs)
|
|
228
|
+
serializer = XmlSerializer(config=config)
|
|
229
|
+
|
|
230
|
+
with path.open("w", encoding="utf-8") as f:
|
|
231
|
+
serializer.write( # type: ignore[reportUnknownMemberType]
|
|
232
|
+
f,
|
|
233
|
+
space_system,
|
|
234
|
+
ns_map={"xtce": xtce_version.value.namespace},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return XtceFile(path)
|
|
238
|
+
|
|
239
|
+
def _index_space_system(
|
|
240
|
+
self,
|
|
241
|
+
space_system: SpaceSystem,
|
|
242
|
+
parent_path: XtcePath,
|
|
243
|
+
registry: XtceRegistry,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Recursively walk the SpaceSystem hierarchy and index all elements."""
|
|
246
|
+
current_path = parent_path / space_system.name
|
|
247
|
+
registry.register(current_path, space_system)
|
|
248
|
+
|
|
249
|
+
collections_to_index: list[Iterable[ReferenceableXtceObject] | None] = []
|
|
250
|
+
|
|
251
|
+
if space_system.command_metadata:
|
|
252
|
+
cmd = space_system.command_metadata
|
|
253
|
+
collections_to_index.extend(
|
|
254
|
+
[
|
|
255
|
+
cmd.argument_types,
|
|
256
|
+
[
|
|
257
|
+
meta_command
|
|
258
|
+
for meta_command in cmd.meta_commands
|
|
259
|
+
if not isinstance(meta_command, MetaCommandRef)
|
|
260
|
+
],
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if space_system.telemetry_metadata:
|
|
265
|
+
tlm = space_system.telemetry_metadata
|
|
266
|
+
collections_to_index.extend(
|
|
267
|
+
[
|
|
268
|
+
tlm.parameter_types,
|
|
269
|
+
[
|
|
270
|
+
parameter
|
|
271
|
+
for parameter in tlm.parameters
|
|
272
|
+
if not isinstance(parameter, ParameterRef)
|
|
273
|
+
],
|
|
274
|
+
tlm.containers,
|
|
275
|
+
tlm.message_set.messages if tlm.message_set else None,
|
|
276
|
+
]
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Iterate through all objects and register in one pass
|
|
280
|
+
valid_collections = (c for c in collections_to_index if c)
|
|
281
|
+
for item in itertools.chain.from_iterable(valid_collections):
|
|
282
|
+
registry.register(current_path / item.name, item)
|
|
283
|
+
|
|
284
|
+
# Recurse into child SpaceSystems
|
|
285
|
+
for child in space_system.space_systems:
|
|
286
|
+
self._index_space_system(child, current_path, registry)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""XTCE file object."""
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
from importlib.abc import Traversable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
9
|
+
|
|
10
|
+
from lxml import etree
|
|
11
|
+
from xsdata.formats.dataclass.parsers import XmlParser
|
|
12
|
+
|
|
13
|
+
from xtce_lib.xtce.space_system import SpaceSystem
|
|
14
|
+
|
|
15
|
+
from .validation import ValidationReport, XtceSchemaError
|
|
16
|
+
from .xtce_version import XtceVersion
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from xtce_lib.generated.xtce_1_1.models import SpaceSystem as SpaceSystem11
|
|
20
|
+
from xtce_lib.generated.xtce_1_2.models import SpaceSystem as SpaceSystem12
|
|
21
|
+
from xtce_lib.generated.xtce_1_3.models import SpaceSystem as SpaceSystem13
|
|
22
|
+
|
|
23
|
+
from .xtce_database import XtceDatabase
|
|
24
|
+
|
|
25
|
+
AnySpaceSystem = SpaceSystem11 | SpaceSystem12 | SpaceSystem13
|
|
26
|
+
else:
|
|
27
|
+
AnySpaceSystem = Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class XtceFile:
|
|
31
|
+
"""XTCE file manager for parsing and validation."""
|
|
32
|
+
|
|
33
|
+
# TODO add unified model
|
|
34
|
+
|
|
35
|
+
# Cache at class level
|
|
36
|
+
_schema_cache: ClassVar[dict[XtceVersion, etree.XMLSchema]] = {}
|
|
37
|
+
_NAMESPACE_PATTERN: ClassVar[re.Pattern[str]] = re.compile(r"^\{([^}]+)\}")
|
|
38
|
+
|
|
39
|
+
def __init__(self, file_path: str | Path) -> None:
|
|
40
|
+
"""Initialize the XTCE file object."""
|
|
41
|
+
self._file_path = Path(file_path).resolve()
|
|
42
|
+
|
|
43
|
+
self._namespace = self.get_namespace(self._file_path)
|
|
44
|
+
self._version = XtceVersion.from_namespace(self._namespace)
|
|
45
|
+
self._raw_model: AnySpaceSystem | None = None
|
|
46
|
+
|
|
47
|
+
self._schema_resource, self._xml_resource = self._get_xsd_resources()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def file_path(self) -> Path:
|
|
51
|
+
"""Get the XTCE file path."""
|
|
52
|
+
return self._file_path
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def namespace(self) -> str:
|
|
56
|
+
"""Get the XML namespace of the XTCE file."""
|
|
57
|
+
return self._namespace
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def version(self) -> str:
|
|
61
|
+
"""Get the XTCE version of the file."""
|
|
62
|
+
return self._version.value.version
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def database(self) -> "XtceDatabase":
|
|
66
|
+
"""Get the XTCE database object for this file."""
|
|
67
|
+
if self._raw_model is None:
|
|
68
|
+
self.parse_raw()
|
|
69
|
+
|
|
70
|
+
from .xtce_database import XtceDatabase
|
|
71
|
+
|
|
72
|
+
return XtceDatabase.from_space_system(
|
|
73
|
+
SpaceSystem.from_xsdata(self._raw_model, self._version)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def raw_model(self) -> Any:
|
|
78
|
+
"""Get the raw parsed model of the XTCE file."""
|
|
79
|
+
if self._raw_model is None:
|
|
80
|
+
self.parse_raw()
|
|
81
|
+
return self._raw_model
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def get_namespace(file_path: str | Path) -> str:
|
|
85
|
+
"""Extract the namespace from the file path.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
file_path (str | Path): The path to the XTCE XML file.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str: The namespace of the XTCE XML file.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If the XML file is invalid or if no namespace is found.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
file_path = Path(file_path)
|
|
98
|
+
try:
|
|
99
|
+
for _, element in etree.iterparse(file_path, events=["start"]):
|
|
100
|
+
if isinstance(element.tag, str):
|
|
101
|
+
match = XtceFile._NAMESPACE_PATTERN.match(element.tag)
|
|
102
|
+
if match:
|
|
103
|
+
return match.group(1)
|
|
104
|
+
|
|
105
|
+
except etree.XMLSyntaxError as e:
|
|
106
|
+
raise ValueError(f"Invalid XML file: {file_path}: {e}")
|
|
107
|
+
|
|
108
|
+
raise ValueError(f"No namespace found in XML file: {file_path}")
|
|
109
|
+
|
|
110
|
+
def validate(self) -> ValidationReport[XtceSchemaError]:
|
|
111
|
+
"""Validate the XTCE file against its corresponding XSD schema.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
ValidationReport[XtceSchemaError]: The result of the validation.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If the XML file is invalid or if validation fails.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
validator = self._get_validator()
|
|
121
|
+
report = ValidationReport[XtceSchemaError](title="XSD Validation")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
xml_doc = etree.parse(self._file_path)
|
|
125
|
+
is_valid = validator.validate(xml_doc)
|
|
126
|
+
|
|
127
|
+
if is_valid:
|
|
128
|
+
return report
|
|
129
|
+
|
|
130
|
+
for error in validator.error_log:
|
|
131
|
+
# Strip the namespace
|
|
132
|
+
clean_message = re.sub(r"\{(?:http|urn)[^}]+\}", "", error.message)
|
|
133
|
+
|
|
134
|
+
report.add_error(
|
|
135
|
+
XtceSchemaError(line=error.line, message=clean_message)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return report
|
|
139
|
+
|
|
140
|
+
except etree.XMLSyntaxError as e:
|
|
141
|
+
# Catastrophic syntax error
|
|
142
|
+
report.add_error(XtceSchemaError(line=e.lineno or 0, message=str(e)))
|
|
143
|
+
return report
|
|
144
|
+
|
|
145
|
+
def parse_raw(self, strict: bool = False) -> None:
|
|
146
|
+
"""Parse the XML file into the version-specific xsdata dataclasses.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
strict: If True, validates the file against the schema before parsing.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ValueError: If validation fails in strict mode or if parsing fails.
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
if strict:
|
|
156
|
+
result = self.validate()
|
|
157
|
+
if not result.is_valid:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Cannot parse invalid XTCE file: {result.errors[0].message}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Import the correct generated module based on the XTCE version
|
|
163
|
+
module_path = f"xtce_lib.generated.{self._version.value.module_name}.models"
|
|
164
|
+
models_module = importlib.import_module(module_path)
|
|
165
|
+
|
|
166
|
+
# Parse XML into the SpaceSystem root class
|
|
167
|
+
root_class = getattr(models_module, "SpaceSystem")
|
|
168
|
+
parser = XmlParser()
|
|
169
|
+
self._raw_model = parser.parse(str(self._file_path), root_class)
|
|
170
|
+
|
|
171
|
+
# TODO catch exceptions?
|
|
172
|
+
|
|
173
|
+
def _get_validator(self) -> etree.XMLSchema:
|
|
174
|
+
"""Get the XMLSchema validator for the XTCE version of this file."""
|
|
175
|
+
if self._version in self._schema_cache:
|
|
176
|
+
return self._schema_cache[self._version]
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Write the XSD resources to a temporary directory so that relative paths
|
|
180
|
+
# like xml.xsd are resolved correctly
|
|
181
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
182
|
+
temp_path = Path(temp_dir)
|
|
183
|
+
|
|
184
|
+
schema_path = temp_path / self._schema_resource.name
|
|
185
|
+
xml_xsd_path = temp_path / self._xml_resource.name
|
|
186
|
+
|
|
187
|
+
schema_path.write_bytes(self._schema_resource.read_bytes())
|
|
188
|
+
xml_xsd_path.write_bytes(self._xml_resource.read_bytes())
|
|
189
|
+
|
|
190
|
+
schema_doc = etree.parse(schema_path)
|
|
191
|
+
xmlschema = etree.XMLSchema(schema_doc)
|
|
192
|
+
|
|
193
|
+
self.__class__._schema_cache[self._version] = xmlschema
|
|
194
|
+
return xmlschema
|
|
195
|
+
|
|
196
|
+
except etree.XMLSchemaParseError as e:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"Failed to compile XSD schema for version {self._version}: {e}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _get_xsd_resources(self) -> tuple[Traversable, Traversable]:
|
|
202
|
+
"""Get schema resources for the XTCE version of this file."""
|
|
203
|
+
files = importlib.resources.files("xtce_lib.xsd")
|
|
204
|
+
schema_file = files / self._version.value.xsd_name
|
|
205
|
+
xsd_file = files / "xml.xsd"
|
|
206
|
+
|
|
207
|
+
return schema_file, xsd_file
|