odxtools 10.2.1__py3-none-any.whl → 10.4.0__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.
Files changed (74) hide show
  1. odxtools/cli/browse.py +4 -2
  2. odxtools/cli/compare.py +3 -3
  3. odxtools/compositecodec.py +1 -1
  4. odxtools/configdata.py +70 -0
  5. odxtools/configdatadictionaryspec.py +57 -0
  6. odxtools/configiditem.py +18 -0
  7. odxtools/configitem.py +85 -0
  8. odxtools/configrecord.py +146 -0
  9. odxtools/database.py +40 -0
  10. odxtools/dataiditem.py +18 -0
  11. odxtools/datarecord.py +132 -0
  12. odxtools/decodestate.py +1 -1
  13. odxtools/diagcommdataconnector.py +61 -0
  14. odxtools/diaglayers/diaglayer.py +9 -14
  15. odxtools/diagservice.py +10 -10
  16. odxtools/ecuconfig.py +89 -0
  17. odxtools/encryptcompressmethod.py +16 -3
  18. odxtools/externflashdata.py +21 -2
  19. odxtools/flashdata.py +40 -2
  20. odxtools/identvalue.py +16 -3
  21. odxtools/internflashdata.py +4 -0
  22. odxtools/isotp_state_machine.py +11 -11
  23. odxtools/itemvalue.py +77 -0
  24. odxtools/multipleecujob.py +178 -0
  25. odxtools/multipleecujobspec.py +142 -0
  26. odxtools/nameditemlist.py +2 -2
  27. odxtools/optionitem.py +79 -0
  28. odxtools/outputparam.py +1 -1
  29. odxtools/parameters/codedconstparameter.py +2 -3
  30. odxtools/readdiagcommconnector.py +79 -0
  31. odxtools/readparamvalue.py +52 -0
  32. odxtools/request.py +3 -3
  33. odxtools/response.py +8 -4
  34. odxtools/snrefcontext.py +2 -0
  35. odxtools/statemachine.py +3 -2
  36. odxtools/systemitem.py +23 -0
  37. odxtools/templates/diag_layer_container.odx-d.xml.jinja2 +4 -0
  38. odxtools/templates/ecu_config.odx-e.xml.jinja2 +38 -0
  39. odxtools/templates/flash.odx-f.xml.jinja2 +2 -2
  40. odxtools/templates/macros/printAdminData.xml.jinja2 +1 -1
  41. odxtools/templates/macros/printAudience.xml.jinja2 +3 -3
  42. odxtools/templates/macros/printChecksum.xml.jinja2 +1 -1
  43. odxtools/templates/macros/printCompuMethod.xml.jinja2 +2 -2
  44. odxtools/templates/macros/printConfigData.xml.jinja2 +39 -0
  45. odxtools/templates/macros/printConfigDataDictionarySpec.xml.jinja2 +22 -0
  46. odxtools/templates/macros/printConfigItems.xml.jinja2 +66 -0
  47. odxtools/templates/macros/printConfigRecord.xml.jinja2 +73 -0
  48. odxtools/templates/macros/printDOP.xml.jinja2 +5 -6
  49. odxtools/templates/macros/printDataRecord.xml.jinja2 +35 -0
  50. odxtools/templates/macros/printDiagCommDataConnector.xml.jinja2 +66 -0
  51. odxtools/templates/macros/printDiagDataDictionarySpec.xml.jinja2 +107 -0
  52. odxtools/templates/macros/printDiagLayer.xml.jinja2 +11 -97
  53. odxtools/templates/macros/printElementId.xml.jinja2 +2 -0
  54. odxtools/templates/macros/printExpectedIdent.xml.jinja2 +1 -1
  55. odxtools/templates/macros/printFlashdata.xml.jinja2 +2 -2
  56. odxtools/templates/macros/printFunctionalClass.xml.jinja2 +4 -4
  57. odxtools/templates/macros/printItemValue.xml.jinja2 +31 -0
  58. odxtools/templates/macros/printMultipleEcuJob.xml.jinja2 +77 -0
  59. odxtools/templates/macros/printOwnIdent.xml.jinja2 +1 -1
  60. odxtools/templates/macros/printSecurity.xml.jinja2 +4 -4
  61. odxtools/templates/macros/printSegment.xml.jinja2 +1 -1
  62. odxtools/templates/multiple-ecu-job-spec.odx-m.xml.jinja2 +51 -0
  63. odxtools/validbasevariant.py +62 -0
  64. odxtools/validityfor.py +16 -3
  65. odxtools/variantmatcher.py +4 -4
  66. odxtools/version.py +2 -2
  67. odxtools/writediagcommconnector.py +77 -0
  68. odxtools/writepdxfile.py +58 -27
  69. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/METADATA +2 -1
  70. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/RECORD +74 -45
  71. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/WHEEL +1 -1
  72. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/entry_points.txt +0 -0
  73. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/licenses/LICENSE +0 -0
  74. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/top_level.txt +0 -0
@@ -44,7 +44,7 @@ class IsoTpStateMachine:
44
44
  self._telegram_data: list[bytearray | None] = [None] * len(can_rx_ids)
45
45
  self._telegram_last_rx_fragment_idx = [0] * len(can_rx_ids)
46
46
 
47
- def decode_rx_frame(self, rx_id: int, data: bytes) -> Iterable[tuple[int, bytes]]:
47
+ def decode_rx_frame(self, rx_id: int, data: bytes | bytearray) -> Iterable[tuple[int, bytes]]:
48
48
  """Handle the ISO-TP state transitions caused by a CAN frame.
49
49
 
50
50
  E.g., add some data to a telegram, etc. Returns a generator of
@@ -67,7 +67,7 @@ class IsoTpStateMachine:
67
67
  self.on_single_frame(telegram_idx, data[1:1 + telegram_len])
68
68
  self.on_telegram_complete(telegram_idx, data[1:1 + telegram_len])
69
69
 
70
- yield (rx_id, data[1:1 + telegram_len])
70
+ yield (rx_id, bytes(data[1:1 + telegram_len]))
71
71
 
72
72
  elif frame_type == IsoTp.FRAME_TYPE_FIRST:
73
73
  frame_type, telegram_len = bitstruct.unpack("u4u12", data)
@@ -105,7 +105,7 @@ class IsoTpStateMachine:
105
105
  self.on_sequence_error(telegram_idx, expected_segment_idx, rx_segment_idx)
106
106
  elif len(telegram_data) == n:
107
107
  self.on_telegram_complete(telegram_idx, telegram_data)
108
- yield (rx_id, telegram_data)
108
+ yield (rx_id, bytes(telegram_data))
109
109
 
110
110
  elif frame_type == IsoTp.FRAME_TYPE_FLOW_CONTROL:
111
111
  frame_type, flow_control_flag = bitstruct.unpack("u4u4", data)
@@ -185,7 +185,7 @@ class IsoTpStateMachine:
185
185
  """
186
186
  return self._can_rx_ids[telegram_idx]
187
187
 
188
- def telegram_data(self, telegram_idx: int) -> bytes | None:
188
+ def telegram_data(self, telegram_idx: int) -> bytearray | None:
189
189
  """Given a Telegram index, returns the data received for this telegram
190
190
  so far.
191
191
 
@@ -196,16 +196,16 @@ class IsoTpStateMachine:
196
196
  ##############
197
197
  # Callbacks
198
198
  ##############
199
- def on_single_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
199
+ def on_single_frame(self, telegram_idx: int, frame_payload: bytes | bytearray) -> None:
200
200
  """Callback method for when an ISO-TP message of type "single frame" has been received"""
201
201
  pass
202
202
 
203
- def on_first_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
203
+ def on_first_frame(self, telegram_idx: int, frame_payload: bytes | bytearray) -> None:
204
204
  """Callback method for when an ISO-TP message of type "first frame" has been received"""
205
205
  pass
206
206
 
207
207
  def on_consecutive_frame(self, telegram_idx: int, segment_idx: int,
208
- frame_payload: bytes) -> None:
208
+ frame_payload: bytes | bytearray) -> None:
209
209
  """Callback method for when an ISO-TP message of type "consecutive frame" has been received"""
210
210
  pass
211
211
 
@@ -221,7 +221,7 @@ class IsoTpStateMachine:
221
221
  """Method called when a frame exhibiting an unknown frame type has been received"""
222
222
  pass
223
223
 
224
- def on_telegram_complete(self, telegram_idx: int, telegram_payload: bytes) -> None:
224
+ def on_telegram_complete(self, telegram_idx: int, telegram_payload: bytes | bytearray) -> None:
225
225
  """Method called when an ISO-TP telegram has been fully received"""
226
226
  pass
227
227
 
@@ -264,7 +264,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
264
264
  """
265
265
  return self._can_tx_ids[telegram_idx]
266
266
 
267
- def on_single_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
267
+ def on_single_frame(self, telegram_idx: int, frame_payload: bytes | bytearray) -> None:
268
268
  # send ACK
269
269
  # rx_id = self.can_rx_id(telegram_idx)
270
270
  tx_id = self.can_tx_id(telegram_idx)
@@ -283,7 +283,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
283
283
 
284
284
  super().on_first_frame(telegram_idx, frame_payload)
285
285
 
286
- def on_first_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
286
+ def on_first_frame(self, telegram_idx: int, frame_payload: bytes | bytearray) -> None:
287
287
  # send ACK
288
288
  # rx_id = self.can_rx_id(telegram_idx)
289
289
  tx_id = self.can_tx_id(telegram_idx)
@@ -306,7 +306,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
306
306
  super().on_first_frame(telegram_idx, frame_payload)
307
307
 
308
308
  def on_consecutive_frame(self, telegram_idx: int, segment_idx: int,
309
- frame_payload: bytes) -> None:
309
+ frame_payload: bytes | bytearray) -> None:
310
310
  num_received = self._frames_received[telegram_idx]
311
311
  if num_received is None:
312
312
  # consequtive frame received before a first frame.
odxtools/itemvalue.py ADDED
@@ -0,0 +1,77 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+ from xml.etree import ElementTree
5
+
6
+ from .audience import Audience
7
+ from .odxdoccontext import OdxDocContext
8
+ from .odxlink import OdxLinkDatabase, OdxLinkId
9
+ from .snrefcontext import SnRefContext
10
+ from .specialdatagroup import SpecialDataGroup
11
+ from .text import Text
12
+
13
+
14
+ @dataclass(kw_only=True)
15
+ class ItemValue:
16
+ """This class represents a ITEM-VALUE."""
17
+
18
+ phys_constant_value: str | None
19
+ meaning: Text | None = None
20
+ key: str | None = None
21
+ rule: str | None = None
22
+ description: Text | None = None
23
+ sdgs: list[SpecialDataGroup] = field(default_factory=list)
24
+ audience: Audience | None = None
25
+
26
+ @staticmethod
27
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "ItemValue":
28
+ phys_constant_value = et_element.findtext("PHYS-CONSTANT-VALUE")
29
+
30
+ meaning = None
31
+ if (meaning_elem := et_element.find("MEANING")) is not None:
32
+ meaning = Text.from_et(meaning_elem, context)
33
+
34
+ key = et_element.findtext("KEY")
35
+ rule = et_element.findtext("RULE")
36
+
37
+ description = None
38
+ if (description_elem := et_element.find("DESCRIPTION")) is not None:
39
+ description = Text.from_et(description_elem, context)
40
+
41
+ sdgs = [SpecialDataGroup.from_et(sdge, context) for sdge in et_element.iterfind("SDGS/SDG")]
42
+
43
+ audience = None
44
+ if (aud_elem := et_element.find("AUDIENCE")) is not None:
45
+ audience = Audience.from_et(aud_elem, context)
46
+
47
+ return ItemValue(
48
+ phys_constant_value=phys_constant_value,
49
+ meaning=meaning,
50
+ key=key,
51
+ rule=rule,
52
+ description=description,
53
+ sdgs=sdgs,
54
+ audience=audience,
55
+ )
56
+
57
+ def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
58
+ result = {}
59
+
60
+ if self.audience is not None:
61
+ result.update(self.audience._build_odxlinks())
62
+ for sdg in self.sdgs:
63
+ result.update(sdg._build_odxlinks())
64
+
65
+ return result
66
+
67
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
68
+ if self.audience is not None:
69
+ self.audience._resolve_odxlinks(odxlinks)
70
+ for sdg in self.sdgs:
71
+ sdg._resolve_odxlinks(odxlinks)
72
+
73
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
74
+ if self.audience is not None:
75
+ self.audience._resolve_snrefs(context)
76
+ for sdg in self.sdgs:
77
+ sdg._resolve_snrefs(context)
@@ -0,0 +1,178 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+ from xml.etree import ElementTree
5
+
6
+ from .admindata import AdminData
7
+ from .audience import Audience
8
+ from .diaglayers.diaglayer import DiagLayer
9
+ from .element import IdentifiableElement
10
+ from .exceptions import odxrequire
11
+ from .functionalclass import FunctionalClass
12
+ from .inputparam import InputParam
13
+ from .nameditemlist import NamedItemList
14
+ from .negoutputparam import NegOutputParam
15
+ from .odxdoccontext import OdxDocContext
16
+ from .odxlink import OdxLinkDatabase, OdxLinkId, OdxLinkRef
17
+ from .odxtypes import odxstr_to_bool
18
+ from .outputparam import OutputParam
19
+ from .progcode import ProgCode
20
+ from .snrefcontext import SnRefContext
21
+ from .specialdatagroup import SpecialDataGroup
22
+ from .utils import dataclass_fields_asdict
23
+
24
+
25
+ @dataclass(kw_only=True)
26
+ class MultipleEcuJob(IdentifiableElement):
27
+ """A multiple ECU job is a diagnostic communication primitive.
28
+
29
+ A multiple ECU job is more complex than a diagnostic service and is
30
+ not provided natively by the ECU. In particular, the job is
31
+ defined in external programs which are referenced by the attribute
32
+ `.prog_codes`.
33
+
34
+ In contrast to "single ECU jobs", a multiple ECU job only involves
35
+ calls to services provided by more than one ECU.
36
+
37
+ Multiple ECU jobs are defined in section 7.3.11 of the ASAM MCD-2
38
+ standard.
39
+ """
40
+
41
+ admin_data: AdminData | None = None
42
+ sdgs: list[SpecialDataGroup] = field(default_factory=list)
43
+ functional_class_refs: list[OdxLinkRef] = field(default_factory=list)
44
+ prog_codes: list[ProgCode] = field(default_factory=list)
45
+ input_params: NamedItemList[InputParam] = field(default_factory=NamedItemList)
46
+ output_params: NamedItemList[OutputParam] = field(default_factory=NamedItemList)
47
+ neg_output_params: NamedItemList[NegOutputParam] = field(default_factory=NamedItemList)
48
+ diag_layer_refs: list[OdxLinkRef] = field(default_factory=list)
49
+ audience: Audience | None = None
50
+ semantic: str | None = None
51
+ is_executable_raw: bool | None = None
52
+
53
+ @property
54
+ def functional_classes(self) -> NamedItemList[FunctionalClass]:
55
+ return self._functional_classes
56
+
57
+ @property
58
+ def diag_layers(self) -> NamedItemList[DiagLayer]:
59
+ return self._diag_layers
60
+
61
+ @property
62
+ def is_executable(self) -> bool:
63
+ return self.is_executable_raw in (True, None)
64
+
65
+ @staticmethod
66
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "MultipleEcuJob":
67
+ kwargs = dataclass_fields_asdict(IdentifiableElement.from_et(et_element, context))
68
+
69
+ admin_data = None
70
+ if (admin_data_elem := et_element.find("ADMIN-DATA")) is not None:
71
+ admin_data = AdminData.from_et(admin_data_elem, context)
72
+ sdgs = [SpecialDataGroup.from_et(sdge, context) for sdge in et_element.iterfind("SDGS/SDG")]
73
+ functional_class_refs = [
74
+ odxrequire(OdxLinkRef.from_et(el, context))
75
+ for el in et_element.iterfind("FUNCT-CLASS-REFS/FUNCT-CLASS-REF")
76
+ ]
77
+ prog_codes = [
78
+ ProgCode.from_et(pc_elem, context)
79
+ for pc_elem in et_element.iterfind("PROG-CODES/PROG-CODE")
80
+ ]
81
+
82
+ input_params = NamedItemList([
83
+ InputParam.from_et(el, context)
84
+ for el in et_element.iterfind("INPUT-PARAMS/INPUT-PARAM")
85
+ ])
86
+ output_params = NamedItemList([
87
+ OutputParam.from_et(el, context)
88
+ for el in et_element.iterfind("OUTPUT-PARAMS/OUTPUT-PARAM")
89
+ ])
90
+ neg_output_params = NamedItemList([
91
+ NegOutputParam.from_et(el, context)
92
+ for el in et_element.iterfind("NEG-OUTPUT-PARAMS/NEG-OUTPUT-PARAM")
93
+ ])
94
+ diag_layer_refs = [
95
+ odxrequire(OdxLinkRef.from_et(el, context))
96
+ for el in et_element.iterfind("DIAG-LAYER-REFS/DIAG-LAYER-REF")
97
+ ]
98
+ audience = None
99
+ if (aud_elem := et_element.find("AUDIENCE")) is not None:
100
+ audience = Audience.from_et(aud_elem, context)
101
+
102
+ semantic = et_element.attrib.get("SEMANTIC")
103
+ is_executable_raw = odxstr_to_bool(et_element.attrib.get("IS-EXECUTABLE"))
104
+
105
+ return MultipleEcuJob(
106
+ admin_data=admin_data,
107
+ sdgs=sdgs,
108
+ functional_class_refs=functional_class_refs,
109
+ prog_codes=prog_codes,
110
+ input_params=input_params,
111
+ output_params=output_params,
112
+ neg_output_params=neg_output_params,
113
+ diag_layer_refs=diag_layer_refs,
114
+ audience=audience,
115
+ semantic=semantic,
116
+ is_executable_raw=is_executable_raw,
117
+ **kwargs)
118
+
119
+ def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
120
+ result = {self.odx_id: self}
121
+
122
+ if self.admin_data is not None:
123
+ result.update(self.admin_data._build_odxlinks())
124
+ for sdg in self.sdgs:
125
+ result.update(sdg._build_odxlinks())
126
+ for prog_code in self.prog_codes:
127
+ result.update(prog_code._build_odxlinks())
128
+ for input_param in self.input_params:
129
+ result.update(input_param._build_odxlinks())
130
+ for output_param in self.output_params:
131
+ result.update(output_param._build_odxlinks())
132
+ for neg_output_param in self.neg_output_params:
133
+ result.update(neg_output_param._build_odxlinks())
134
+ if self.audience is not None:
135
+ result.update(self.audience._build_odxlinks())
136
+
137
+ return result
138
+
139
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
140
+ if self.admin_data is not None:
141
+ self.admin_data._resolve_odxlinks(odxlinks)
142
+ for sdg in self.sdgs:
143
+ sdg._resolve_odxlinks(odxlinks)
144
+ for prog_code in self.prog_codes:
145
+ prog_code._resolve_odxlinks(odxlinks)
146
+ for input_param in self.input_params:
147
+ input_param._resolve_odxlinks(odxlinks)
148
+ for output_param in self.output_params:
149
+ output_param._resolve_odxlinks(odxlinks)
150
+ for neg_output_param in self.neg_output_params:
151
+ neg_output_param._resolve_odxlinks(odxlinks)
152
+ if self.audience is not None:
153
+ self.audience._resolve_odxlinks(odxlinks)
154
+
155
+ self._functional_classes = NamedItemList(
156
+ [odxlinks.resolve(fc_ref, FunctionalClass) for fc_ref in self.functional_class_refs])
157
+ self._diag_layers = NamedItemList(
158
+ [odxlinks.resolve(dl_ref, DiagLayer) for dl_ref in self.diag_layer_refs])
159
+
160
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
161
+ context.multiple_ecu_job = self
162
+
163
+ if self.admin_data is not None:
164
+ self.admin_data._resolve_snrefs(context)
165
+ for sdg in self.sdgs:
166
+ sdg._resolve_snrefs(context)
167
+ for prog_code in self.prog_codes:
168
+ prog_code._resolve_snrefs(context)
169
+ for input_param in self.input_params:
170
+ input_param._resolve_snrefs(context)
171
+ for output_param in self.output_params:
172
+ output_param._resolve_snrefs(context)
173
+ for neg_output_param in self.neg_output_params:
174
+ neg_output_param._resolve_snrefs(context)
175
+ if self.audience is not None:
176
+ self.audience._resolve_snrefs(context)
177
+
178
+ context.multiple_ecu_job = None
@@ -0,0 +1,142 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from copy import copy
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any
5
+ from xml.etree import ElementTree
6
+
7
+ from .additionalaudience import AdditionalAudience
8
+ from .diagdatadictionaryspec import DiagDataDictionarySpec
9
+ from .diaglayers.ecushareddata import EcuSharedData
10
+ from .functionalclass import FunctionalClass
11
+ from .multipleecujob import MultipleEcuJob
12
+ from .nameditemlist import NamedItemList
13
+ from .odxcategory import OdxCategory
14
+ from .odxdoccontext import OdxDocContext
15
+ from .odxlink import OdxLinkDatabase, OdxLinkId, OdxLinkRef
16
+ from .snrefcontext import SnRefContext
17
+ from .utils import dataclass_fields_asdict
18
+
19
+ if TYPE_CHECKING:
20
+ from .database import Database
21
+
22
+
23
+ @dataclass(kw_only=True)
24
+ class MultipleEcuJobSpec(OdxCategory):
25
+ multiple_ecu_jobs: NamedItemList[MultipleEcuJob] = field(default_factory=NamedItemList)
26
+ diag_data_dictionary_spec: DiagDataDictionarySpec | None = None
27
+ functional_classes: NamedItemList[FunctionalClass] = field(default_factory=NamedItemList)
28
+ additional_audiences: NamedItemList[AdditionalAudience] = field(default_factory=NamedItemList)
29
+ import_refs: list[OdxLinkRef] = field(default_factory=list)
30
+
31
+ @property
32
+ def imported_layers(self) -> NamedItemList[EcuSharedData]:
33
+ """The resolved IMPORT-REFs
34
+
35
+ ODXLINK defined by ECU-SHARED-DATA layers referenced via
36
+ IMPORT-REF ought to be treated as if they where defined
37
+ locally.
38
+ """
39
+ return self._imported_layers
40
+
41
+ @staticmethod
42
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "MultipleEcuJobSpec":
43
+
44
+ base_obj = OdxCategory.from_et(et_element, context)
45
+ kwargs = dataclass_fields_asdict(base_obj)
46
+
47
+ multiple_ecu_jobs = NamedItemList([
48
+ MultipleEcuJob.from_et(el, context)
49
+ for el in et_element.iterfind("MULTIPLE-ECU-JOBS/MULTIPLE-ECU-JOB")
50
+ ])
51
+ diag_data_dictionary_spec = None
52
+ if (ddds_elem := et_element.find("DIAG-DATA-DICTIONARY-SPEC")) is not None:
53
+ diag_data_dictionary_spec = DiagDataDictionarySpec.from_et(ddds_elem, context)
54
+ functional_classes = NamedItemList([
55
+ FunctionalClass.from_et(el, context)
56
+ for el in et_element.iterfind("FUNCT-CLASSS/FUNCT-CLASS")
57
+ ])
58
+ additional_audiences = NamedItemList([
59
+ AdditionalAudience.from_et(el, context)
60
+ for el in et_element.iterfind("ADDITIONAL-AUDIENCES/ADDITIONAL-AUDIENCE")
61
+ ])
62
+ import_refs = [
63
+ OdxLinkRef.from_et(el, context) for el in et_element.iterfind("IMPORT-REFS/IMPORT-REF")
64
+ ]
65
+
66
+ return MultipleEcuJobSpec(
67
+ multiple_ecu_jobs=multiple_ecu_jobs,
68
+ diag_data_dictionary_spec=diag_data_dictionary_spec,
69
+ functional_classes=functional_classes,
70
+ additional_audiences=additional_audiences,
71
+ import_refs=import_refs,
72
+ **kwargs)
73
+
74
+ def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
75
+ odxlinks = super()._build_odxlinks()
76
+
77
+ for multiple_ecu_job in self.multiple_ecu_jobs:
78
+ odxlinks.update(multiple_ecu_job._build_odxlinks())
79
+
80
+ if self.diag_data_dictionary_spec is not None:
81
+ odxlinks.update(self.diag_data_dictionary_spec._build_odxlinks())
82
+
83
+ for functional_class in self.functional_classes:
84
+ odxlinks.update(functional_class._build_odxlinks())
85
+
86
+ for additional_audience in self.additional_audiences:
87
+ odxlinks.update(additional_audience._build_odxlinks())
88
+
89
+ return odxlinks
90
+
91
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
92
+ # deal with the import refs: IDs defined therein are to be
93
+ # handled like they were local.
94
+ self._imported_layers: NamedItemList[EcuSharedData] = NamedItemList()
95
+ if self.import_refs:
96
+ extended_odxlinks = copy(odxlinks)
97
+ imported_links: dict[OdxLinkId, Any] = {}
98
+ for import_ref in self.import_refs:
99
+ imported_dl = odxlinks.resolve(import_ref, EcuSharedData)
100
+ self._imported_layers.append(imported_dl)
101
+
102
+ # replace the document fragments of the ODX id with
103
+ # the those of the muliple-ecu-spec. (be aware that
104
+ # the "original" locations are still available.)
105
+ imported_dl_links = imported_dl._build_odxlinks()
106
+ for link_id, obj in imported_dl_links.items():
107
+ link_id = OdxLinkId(link_id.local_id, self.odx_id.doc_fragments)
108
+ imported_links[link_id] = obj
109
+
110
+ extended_odxlinks.update(imported_links, overwrite=False)
111
+ else:
112
+ extended_odxlinks = odxlinks
113
+
114
+ for multiple_ecu_job in self.multiple_ecu_jobs:
115
+ multiple_ecu_job._resolve_odxlinks(extended_odxlinks)
116
+
117
+ if self.diag_data_dictionary_spec is not None:
118
+ self.diag_data_dictionary_spec._resolve_odxlinks(extended_odxlinks)
119
+
120
+ for functional_class in self.functional_classes:
121
+ functional_class._resolve_odxlinks(extended_odxlinks)
122
+
123
+ for additional_audiences in self.additional_audiences:
124
+ additional_audiences._resolve_odxlinks(extended_odxlinks)
125
+
126
+ def _finalize_init(self, database: "Database", odxlinks: OdxLinkDatabase) -> None:
127
+ super()._finalize_init(database, odxlinks)
128
+
129
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
130
+ super()._resolve_snrefs(context)
131
+
132
+ for multiple_ecu_job in self.multiple_ecu_jobs:
133
+ multiple_ecu_job._resolve_snrefs(context)
134
+
135
+ if self.diag_data_dictionary_spec is not None:
136
+ self.diag_data_dictionary_spec._resolve_snrefs(context)
137
+
138
+ for functional_class in self.functional_classes:
139
+ functional_class._resolve_snrefs(context)
140
+
141
+ for additional_audiences in self.additional_audiences:
142
+ additional_audiences._resolve_snrefs(context)
odxtools/nameditemlist.py CHANGED
@@ -4,7 +4,7 @@ import typing
4
4
  from collections.abc import Collection, Iterable
5
5
  from copy import deepcopy
6
6
  from keyword import iskeyword
7
- from typing import Any, SupportsIndex, TypeVar, cast, overload, runtime_checkable
7
+ from typing import Any, SupportsIndex, TypeVar, overload, runtime_checkable
8
8
 
9
9
  from .exceptions import odxraise
10
10
 
@@ -159,7 +159,7 @@ class ItemAttributeList(list[T]):
159
159
  return super().__getitem__(key)
160
160
  return default
161
161
  else:
162
- return cast(T | None, self._item_dict.get(key, default))
162
+ return self._item_dict.get(key, default)
163
163
 
164
164
  def __eq__(self, other: object) -> bool:
165
165
  """
odxtools/optionitem.py ADDED
@@ -0,0 +1,79 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+ from xml.etree import ElementTree
5
+
6
+ from .audience import Audience
7
+ from .configitem import ConfigItem
8
+ from .itemvalue import ItemValue
9
+ from .odxdoccontext import OdxDocContext
10
+ from .odxlink import OdxLinkDatabase, OdxLinkId
11
+ from .snrefcontext import SnRefContext
12
+ from .utils import dataclass_fields_asdict
13
+
14
+
15
+ @dataclass(kw_only=True)
16
+ class OptionItem(ConfigItem):
17
+ """This class represents a OPTION-ITEM."""
18
+
19
+ physical_default_value: str | None = None
20
+ item_values: list[ItemValue] = field(default_factory=list)
21
+ write_audience: Audience | None = None
22
+ read_audience: Audience | None = None
23
+
24
+ @staticmethod
25
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "OptionItem":
26
+ kwargs = dataclass_fields_asdict(ConfigItem.from_et(et_element, context))
27
+
28
+ physical_default_value = et_element.findtext("PHYSICAL-DEFAULT-VALUE")
29
+
30
+ item_values = [
31
+ ItemValue.from_et(el, context) for el in et_element.iterfind("ITEM-VALUES/ITEM-VALUE")
32
+ ]
33
+
34
+ write_audience = None
35
+ if (wa_elem := et_element.find("WRITE-AUDIENCE")) is not None:
36
+ write_audience = Audience.from_et(wa_elem, context)
37
+
38
+ read_audience = None
39
+ if (ra_elem := et_element.find("READ-AUDIENCE")) is not None:
40
+ read_audience = Audience.from_et(ra_elem, context)
41
+
42
+ return OptionItem(
43
+ physical_default_value=physical_default_value,
44
+ item_values=item_values,
45
+ write_audience=write_audience,
46
+ read_audience=read_audience,
47
+ **kwargs)
48
+
49
+ def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
50
+ result = super()._build_odxlinks()
51
+
52
+ for item_value in self.item_values:
53
+ result.update(item_value._build_odxlinks())
54
+ if self.write_audience is not None:
55
+ result.update(self.write_audience._build_odxlinks())
56
+ if self.read_audience is not None:
57
+ result.update(self.read_audience._build_odxlinks())
58
+
59
+ return result
60
+
61
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
62
+ super()._resolve_odxlinks(odxlinks)
63
+
64
+ for item_value in self.item_values:
65
+ item_value._resolve_odxlinks(odxlinks)
66
+ if self.write_audience is not None:
67
+ self.write_audience._resolve_odxlinks(odxlinks)
68
+ if self.read_audience is not None:
69
+ self.read_audience._resolve_odxlinks(odxlinks)
70
+
71
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
72
+ super()._resolve_snrefs(context)
73
+
74
+ for item_value in self.item_values:
75
+ item_value._resolve_snrefs(context)
76
+ if self.write_audience is not None:
77
+ self.write_audience._resolve_snrefs(context)
78
+ if self.read_audience is not None:
79
+ self.read_audience._resolve_snrefs(context)
odxtools/outputparam.py CHANGED
@@ -38,7 +38,7 @@ class OutputParam(IdentifiableElement):
38
38
  return OutputParam(dop_base_ref=dop_base_ref, semantic=semantic, **kwargs)
39
39
 
40
40
  def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
41
- return {}
41
+ return {self.odx_id: self}
42
42
 
43
43
  def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
44
44
  self._dop = odxlinks.resolve(self.dop_base_ref, DopBase)
@@ -1,7 +1,7 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  import warnings
3
3
  from dataclasses import dataclass
4
- from typing import Any, cast
4
+ from typing import Any
5
5
  from xml.etree import ElementTree
6
6
 
7
7
  from typing_extensions import override
@@ -57,8 +57,7 @@ class CodedConstParameter(Parameter):
57
57
  coded_value_raw=coded_value_raw, diag_coded_type=diag_coded_type, **kwargs)
58
58
 
59
59
  def __post_init__(self) -> None:
60
- self._coded_value = cast(
61
- AtomicOdxType, self.diag_coded_type.base_data_type.from_string(self.coded_value_raw))
60
+ self._coded_value = self.diag_coded_type.base_data_type.from_string(self.coded_value_raw)
62
61
 
63
62
  @override
64
63
  def _build_odxlinks(self) -> dict[OdxLinkId, Any]: