odxtools 8.3.4__py3-none-any.whl → 9.0.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.
@@ -1,10 +1,12 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  import re
3
3
  import textwrap
4
- from typing import Any, Callable, Dict, List, Optional, Union
4
+ from typing import List, Optional, Tuple, Union
5
5
 
6
6
  import markdownify
7
- from tabulate import tabulate # TODO: switch to rich tables
7
+ from rich import print as rich_print
8
+ from rich.padding import Padding as RichPadding
9
+ from rich.table import Table as RichTable
8
10
 
9
11
  from ..description import Description
10
12
  from ..diaglayers.diaglayer import DiagLayer
@@ -36,148 +38,179 @@ def print_diagnostic_service(service: DiagService,
36
38
  print_pre_condition_states: bool = False,
37
39
  print_state_transitions: bool = False,
38
40
  print_audiences: bool = False,
39
- allow_unknown_bit_lengths: bool = False,
40
- print_fn: Callable[..., Any] = print) -> None:
41
+ allow_unknown_bit_lengths: bool = False) -> None:
41
42
 
42
- print_fn(f" Service '{service.short_name}':")
43
+ rich_print(f" Service '{service.short_name}':")
43
44
 
44
45
  if service.description:
45
46
  desc = format_desc(service.description, indent=3)
46
- print_fn(f" Description: " + desc)
47
+ rich_print(f" Description: " + desc)
47
48
 
48
49
  if print_pre_condition_states and len(service.pre_condition_states) > 0:
49
50
  pre_condition_states_short_names = [
50
51
  pre_condition_state.short_name for pre_condition_state in service.pre_condition_states
51
52
  ]
52
- print_fn(f" Pre-Condition States: {', '.join(pre_condition_states_short_names)}")
53
+ rich_print(f" Pre-Condition States: {', '.join(pre_condition_states_short_names)}")
53
54
 
54
55
  if print_state_transitions and len(service.state_transitions) > 0:
55
56
  state_transitions = [
56
57
  f"{state_transition.source_snref} -> {state_transition.target_snref}"
57
58
  for state_transition in service.state_transitions
58
59
  ]
59
- print_fn(f" State Transitions: {', '.join(state_transitions)}")
60
+ rich_print(f" State Transitions: {', '.join(state_transitions)}")
60
61
 
61
62
  if print_audiences and service.audience:
62
63
  enabled_audiences_short_names = [
63
64
  enabled_audience.short_name for enabled_audience in service.audience.enabled_audiences
64
65
  ]
65
- print_fn(f" Enabled Audiences: {', '.join(enabled_audiences_short_names)}")
66
+ rich_print(f" Enabled Audiences: {', '.join(enabled_audiences_short_names)}")
66
67
 
67
68
  if print_params:
68
- print_service_parameters(
69
- service, allow_unknown_bit_lengths=allow_unknown_bit_lengths, print_fn=print_fn)
69
+ print_service_parameters(service, allow_unknown_bit_lengths=allow_unknown_bit_lengths)
70
70
 
71
71
 
72
72
  def print_service_parameters(service: DiagService,
73
- allow_unknown_bit_lengths: bool = False,
74
- print_fn: Callable[..., Any] = print) -> None:
75
- # prints parameter details of request, positive response and negative response of diagnostic service
73
+ *,
74
+ allow_unknown_bit_lengths: bool = False) -> None:
75
+ # prints parameter details of request, positive response and
76
+ # negative response of diagnostic service
76
77
 
77
78
  # Request
78
79
  if service.request:
79
- print_fn(f" Request '{service.request.short_name}':")
80
+ rich_print(f" Request '{service.request.short_name}':")
80
81
  const_prefix = service.request.coded_const_prefix()
81
- print_fn(
82
+ rich_print(
82
83
  f" Identifying Prefix: 0x{const_prefix.hex().upper()} ({bytes(const_prefix)!r})")
83
- print_fn(f" Parameters:")
84
- table = extract_parameter_tabulation_data(list(service.request.parameters))
85
- table_str = textwrap.indent(tabulate(table, headers='keys', tablefmt='presto'), " ")
86
- print_fn()
87
- print_fn(table_str)
88
- print_fn()
84
+ rich_print(f" Parameters:")
85
+ param_table = extract_parameter_tabulation_data(service.request.parameters)
86
+ rich_print(RichPadding(param_table, pad=(0, 0, 0, 4)))
87
+ rich_print()
89
88
  else:
90
- print_fn(f" No Request!")
89
+ rich_print(f" No Request!")
91
90
 
92
91
  # Positive Responses
93
92
  if not service.positive_responses:
94
- print_fn(f" No positive responses")
93
+ rich_print(f" No positive responses")
95
94
 
96
95
  for resp in service.positive_responses:
97
- print_fn(f" Positive Response '{resp.short_name}':")
98
- print_fn(f" Parameters:\n")
96
+ rich_print(f" Positive Response '{resp.short_name}':")
97
+ rich_print(f" Parameters:\n")
99
98
  table = extract_parameter_tabulation_data(list(resp.parameters))
100
- table_str = textwrap.indent(tabulate(table, headers='keys', tablefmt='presto'), " ")
101
- print_fn(table_str)
102
- print_fn()
99
+ rich_print(RichPadding(table, pad=(0, 0, 0, 4)))
100
+ rich_print()
103
101
 
104
102
  # Negative Response
105
103
  if not service.negative_responses:
106
- print_fn(f" No negative responses")
104
+ rich_print(f" No negative responses")
107
105
 
108
106
  for resp in service.negative_responses:
109
- print_fn(f" Negative Response '{resp.short_name}':")
110
- print_fn(f" Parameters:\n")
107
+ rich_print(f" Negative Response '{resp.short_name}':")
108
+ rich_print(f" Parameters:\n")
111
109
  table = extract_parameter_tabulation_data(list(resp.parameters))
112
- table_str = textwrap.indent(tabulate(table, headers='keys', tablefmt='presto'), " ")
113
- print_fn(table_str)
114
- print_fn()
110
+ rich_print(RichPadding(table, pad=(0, 0, 0, 4)))
111
+ rich_print()
115
112
 
116
- print_fn("\n")
113
+ rich_print("\n")
117
114
 
118
115
 
119
- def extract_service_tabulation_data(services: List[DiagService]) -> Dict[str, Any]:
120
- # extracts data of diagnostic services into Dictionary which can be printed by tabulate module
121
- # TODO: consider indentation
116
+ def extract_service_tabulation_data(services: List[DiagService],
117
+ *,
118
+ additional_columns: Optional[List[Tuple[str, List[str]]]] = None
119
+ ) -> RichTable:
120
+ """Extracts data of diagnostic services into Dictionary which can
121
+ be printed by tabulate module
122
+ """
122
123
 
123
- name = []
124
- semantic = []
125
- request: List[Optional[str]] = []
124
+ # Create Rich table
125
+ table = RichTable(
126
+ title="", show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
127
+
128
+ name_column: List[str] = []
129
+ semantic_column: List[str] = []
130
+ request_column: List[str] = []
126
131
 
127
132
  for service in services:
128
- name.append(service.short_name)
129
- semantic.append(service.semantic)
133
+ name_column.append(service.short_name)
134
+ semantic_column.append(service.semantic or "")
130
135
 
131
136
  if service.request:
132
137
  prefix = service.request.coded_const_prefix()
133
- request.append(f"0x{str(prefix.hex().upper())[:32]}...") if len(
134
- prefix) > 32 else request.append(f"0x{str(prefix.hex().upper())}")
138
+ request_column.append(f"0x{str(prefix.hex().upper())[:32]}...") if len(
139
+ prefix) > 32 else request_column.append(f"0x{str(prefix.hex().upper())}")
135
140
  else:
136
- request.append(None)
137
-
138
- return {'Name': name, 'Semantic': semantic, 'Hex-Request': request}
139
-
140
-
141
- def extract_parameter_tabulation_data(parameters: List[Parameter]) -> Dict[str, Any]:
142
- # extracts data of parameters of diagnostic services into Dictionary which can be printed by tabulate module
143
- # TODO: consider indentation
144
-
145
- name = []
146
- byte = []
147
- bit_length: List[Optional[int]] = []
148
- semantic = []
149
- param_type = []
150
- value: List[Optional[str]] = []
151
- value_type: List[Optional[str]] = []
152
- data_type: List[Optional[str]] = []
153
- dop: List[Optional[str]] = []
141
+ request_column.append("")
142
+
143
+ table.add_column("Name", style="green")
144
+ table.add_column("Semantic", justify="left", style="white")
145
+ table.add_column("Request", justify="left", style="white")
146
+ if additional_columns is not None:
147
+ for ac_title, _ in additional_columns:
148
+ table.add_column(ac_title, justify="left", style="white")
149
+
150
+ rows = zip(name_column, semantic_column, request_column,
151
+ *[ac[1] for ac in additional_columns])
152
+ for row in rows:
153
+ table.add_row(*map(str, row))
154
+
155
+ return table
156
+
157
+
158
+ def extract_parameter_tabulation_data(parameters: List[Parameter]) -> RichTable:
159
+ # extracts data of parameters of diagnostic services into
160
+ # a RichTable object that can be printed
161
+
162
+ # Create Rich table
163
+ table = RichTable(
164
+ title="", show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
165
+
166
+ # Add columns with appropriate styling
167
+ table.add_column("Name", style="green")
168
+ table.add_column("Byte Position", justify="right", style="yellow")
169
+ table.add_column("Bit Length", justify="right", style="yellow")
170
+ table.add_column("Semantic", justify="left", style="white")
171
+ table.add_column("Parameter Type", justify="left", style="white")
172
+ table.add_column("Data Type", justify="left", style="white")
173
+ table.add_column("Value", justify="left", style="yellow")
174
+ table.add_column("Value Type", justify="left", style="white")
175
+ table.add_column("Linked DOP", justify="left", style="white")
176
+
177
+ name_column: List[str] = []
178
+ byte_column: List[str] = []
179
+ bit_length_column: List[str] = []
180
+ semantic_column: List[str] = []
181
+ param_type_column: List[str] = []
182
+ value_column: List[str] = []
183
+ value_type_column: List[str] = []
184
+ data_type_column: List[str] = []
185
+ dop_column: List[str] = []
154
186
 
155
187
  for param in parameters:
156
- name.append(param.short_name)
157
- byte.append(param.byte_position)
158
- semantic.append(param.semantic)
159
- param_type.append(param.parameter_type)
188
+ name_column.append(param.short_name)
189
+ byte_column.append("" if param.byte_position is None else str(param.byte_position))
190
+ semantic_column.append(param.semantic or "")
191
+ param_type_column.append(param.parameter_type)
160
192
  length = 0
161
193
  if param.get_static_bit_length() is not None:
162
- bit_length.append(param.get_static_bit_length())
163
- length = (param.get_static_bit_length() or 0) // 4
194
+ n = param.get_static_bit_length()
195
+ bit_length_column.append("" if n is None else str(n))
196
+ length = (n or 0) // 4
164
197
  else:
165
- bit_length.append(None)
198
+ bit_length_column.append("")
166
199
  if isinstance(param, CodedConstParameter):
167
200
  if isinstance(param.coded_value, int):
168
- value.append(f"0x{param.coded_value:0{length}X}")
201
+ value_column.append(f"0x{param.coded_value:0{length}X}")
169
202
  elif isinstance(param.coded_value, bytes) or isinstance(param.coded_value, bytearray):
170
- value.append(f"0x{param.coded_value.hex().upper()}")
203
+ value_column.append(f"0x{param.coded_value.hex().upper()}")
171
204
  else:
172
- value.append(f"{param.coded_value!r}")
173
- data_type.append(param.diag_coded_type.base_data_type.name)
174
- value_type.append('coded value')
175
- dop.append(None)
205
+ value_column.append(f"{param.coded_value!r}")
206
+ data_type_column.append(param.diag_coded_type.base_data_type.name)
207
+ value_type_column.append('coded value')
208
+ dop_column.append("")
176
209
  elif isinstance(param, NrcConstParameter):
177
- data_type.append(param.diag_coded_type.base_data_type.name)
178
- value.append(str(param.coded_values))
179
- value_type.append('coded values')
180
- dop.append(None)
210
+ data_type_column.append(param.diag_coded_type.base_data_type.name)
211
+ value_column.append(str(param.coded_values))
212
+ value_type_column.append('coded values')
213
+ dop_column.append("")
181
214
  elif isinstance(param, (PhysicalConstantParameter, SystemParameter, ValueParameter)):
182
215
  # this is a hack to make this routine work for parameters
183
216
  # which reference DOPs of a type that a is not yet
@@ -186,72 +219,71 @@ def extract_parameter_tabulation_data(parameters: List[Parameter]) -> Dict[str,
186
219
  param_dop = getattr(param, "_dop", None)
187
220
 
188
221
  if param_dop is not None:
189
- dop.append(param_dop.short_name)
222
+ dop_column.append(param_dop.short_name)
190
223
 
191
224
  if param_dop is not None and (phys_type := getattr(param, "physical_type",
192
225
  None)) is not None:
193
- data_type.append(phys_type.base_data_type.name)
226
+ data_type_column.append(phys_type.base_data_type.name)
194
227
  else:
195
- data_type.append(None)
228
+ data_type_column.append("")
196
229
  if isinstance(param, PhysicalConstantParameter):
197
230
  if isinstance(param.physical_constant_value, bytes) or isinstance(
198
231
  param.physical_constant_value, bytearray):
199
- value.append(f"0x{param.physical_constant_value.hex().upper()}")
232
+ value_column.append(f"0x{param.physical_constant_value.hex().upper()}")
200
233
  else:
201
- value.append(f"{param.physical_constant_value!r}")
202
- value_type.append('constant value')
234
+ value_column.append(f"{param.physical_constant_value!r}")
235
+ value_type_column.append('constant value')
203
236
  elif isinstance(param, ValueParameter) and param.physical_default_value is not None:
204
237
  if isinstance(param.physical_default_value, bytes) or isinstance(
205
238
  param.physical_default_value, bytearray):
206
- value.append(f"0x{param.physical_default_value.hex().upper()}")
239
+ value_column.append(f"0x{param.physical_default_value.hex().upper()}")
207
240
  else:
208
- value.append(f"{param.physical_default_value!r}")
209
- value_type.append('default value')
241
+ value_column.append(f"{param.physical_default_value!r}")
242
+ value_type_column.append('default value')
210
243
  else:
211
- value.append(None)
212
- value_type.append(None)
244
+ value_column.append("")
245
+ value_type_column.append("")
213
246
  else:
214
- value.append(None)
215
- data_type.append(None)
216
- value_type.append(None)
217
- dop.append(None)
218
-
219
- return {
220
- 'Name': name,
221
- 'Byte Position': byte,
222
- 'Bit Length': bit_length,
223
- 'Semantic': semantic,
224
- 'Parameter Type': param_type,
225
- 'Data Type': data_type,
226
- 'Value': value,
227
- 'Value Type': value_type,
228
- 'Linked DOP': dop
229
- }
230
-
231
-
232
- def print_dl_metrics(variants: List[DiagLayer], print_fn: Callable[..., Any] = print) -> None:
233
-
234
- name = []
235
- type = []
236
- num_services = []
237
- num_dops = []
238
- num_comparam_refs = []
247
+ value_column.append("")
248
+ data_type_column.append("")
249
+ value_type_column.append("")
250
+ dop_column.append("")
251
+
252
+ # Add all rows at once by zipping dictionary values
253
+ rows = zip(name_column, byte_column, bit_length_column, semantic_column, param_type_column,
254
+ data_type_column, value_column, value_type_column, dop_column)
255
+ for row in rows:
256
+ table.add_row(*map(str, row))
257
+
258
+ return table
259
+
260
+
261
+ def print_dl_metrics(variants: List[DiagLayer]) -> None:
262
+ """
263
+ Print diagnostic layer metrics using Rich tables.
264
+ Args:
265
+ variants: List of diagnostic layer variants to analyze
266
+ """
267
+ # Create Rich table
268
+ table = RichTable(
269
+ title="", show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
270
+
271
+ # Add columns with appropriate styling
272
+ table.add_column("Name", style="green")
273
+ table.add_column("Variant Type", style="magenta")
274
+ table.add_column("Number of Services", justify="right", style="yellow")
275
+ table.add_column("Number of DOPs", justify="right", style="yellow")
276
+ table.add_column("Number of communication parameters", justify="right", style="yellow")
277
+
278
+ # Process each variant
239
279
  for variant in variants:
240
280
  assert isinstance(variant, DiagLayer)
241
281
  all_services: List[Union[DiagService, SingleEcuJob]] = sorted(
242
282
  variant.services, key=lambda x: x.short_name)
243
- name.append(variant.short_name)
244
- type.append(variant.variant_type.value)
245
- num_services.append(len(all_services))
246
283
  ddds = variant.diag_data_dictionary_spec
247
- num_dops.append(len(ddds.data_object_props))
248
- num_comparam_refs.append(len(getattr(variant, "comparams_refs", [])))
249
-
250
- table = {
251
- 'Name': name,
252
- 'Variant Type': type,
253
- 'Number of Services': num_services,
254
- 'Number of DOPs': num_dops,
255
- 'Number of communication parameters': num_comparam_refs
256
- }
257
- print_fn(tabulate(table, headers='keys', tablefmt='presto'))
284
+
285
+ # Add row to table
286
+ table.add_row(variant.short_name, variant.variant_type.value, str(len(all_services)),
287
+ str(len(ddds.data_object_props)),
288
+ str(len(getattr(variant, "comparams_refs", []))))
289
+ rich_print(table)
odxtools/cli/browse.py CHANGED
@@ -5,14 +5,16 @@ import sys
5
5
  from typing import List, Optional, Union, cast
6
6
 
7
7
  import InquirerPy.prompt as IP_prompt
8
- from tabulate import tabulate # TODO: switch to rich tables
9
8
 
9
+ from ..complexdop import ComplexDop
10
10
  from ..database import Database
11
11
  from ..dataobjectproperty import DataObjectProperty
12
12
  from ..diaglayer import DiagLayer
13
13
  from ..diagservice import DiagService
14
+ from ..dopbase import DopBase
14
15
  from ..exceptions import odxraise, odxrequire
15
16
  from ..hierarchyelement import HierarchyElement
17
+ from ..odxlink import resolve_snref
16
18
  from ..odxtypes import AtomicOdxType, DataType, ParameterValueDict
17
19
  from ..parameters.matchingrequestparameter import MatchingRequestParameter
18
20
  from ..parameters.parameter import Parameter
@@ -111,24 +113,44 @@ def prompt_single_parameter_value(parameter: Parameter) -> Optional[AtomicOdxTyp
111
113
  return cast(str, answer.get(parameter.short_name))
112
114
 
113
115
 
114
- def encode_message_interactively(sub_service: Union[Request, Response],
116
+ def encode_message_interactively(codec: Union[Request, Response],
115
117
  ask_user_confirmation: bool = False) -> None:
116
118
  if sys.__stdin__ is None or sys.__stdout__ is None or not sys.__stdin__.isatty(
117
119
  ) or not sys.__stdout__.isatty():
118
120
  raise SystemError("This command can only be used in an interactive shell!")
119
- param_dict = sub_service.parameter_dict()
120
121
 
121
- exists_definable_param = False
122
- for param_or_dict in param_dict.values():
123
- if isinstance(param_or_dict, dict):
124
- for param in param_or_dict.values():
125
- if isinstance(param, Parameter) and param.is_settable:
126
- exists_definable_param = True
127
- elif param_or_dict.is_settable:
128
- exists_definable_param = True
122
+ answered_request = b''
123
+ if isinstance(codec, Response):
124
+ answered_request_prompt = [{
125
+ "type":
126
+ "input",
127
+ "name":
128
+ "request",
129
+ "message":
130
+ f"What is the request you want to answer? (Enter the coded request as integer in hexadecimal format (e.g. 12 3B 5)",
131
+ "filter":
132
+ lambda input: _convert_string_to_bytes(input),
133
+ }]
134
+ answer = IP_prompt(answered_request_prompt)
135
+ answered_request = cast(bytes, answer.get("request"))
136
+ print(f"Input interpretation as list: {list(answered_request)}")
137
+
138
+ has_settable_param = False
139
+ for param in codec.parameters:
140
+ if not param.is_settable:
141
+ continue
142
+
143
+ # TODO: Specifying complex parameters with nesting depth > 1
144
+ # is not possible yet
145
+ if (inner_params := getattr(getattr(param, "dop", None), "parameters", None)) is not None:
146
+ for inner_param in inner_params:
147
+ if inner_param.is_settable:
148
+ has_settable_param = True
149
+ elif param.is_settable:
150
+ has_settable_param = True
129
151
 
130
152
  param_values: ParameterValueDict = {}
131
- if exists_definable_param > 0:
153
+ if has_settable_param:
132
154
  # Ask whether user wants to encode a message
133
155
  if ask_user_confirmation:
134
156
  encode_message_prompt = [{
@@ -141,48 +163,40 @@ def encode_message_interactively(sub_service: Union[Request, Response],
141
163
  if answer.get("yes_no_prompt") == "no":
142
164
  return
143
165
 
144
- answered_request = b''
145
- if isinstance(sub_service, Response):
146
- answered_request_prompt = [{
147
- "type":
148
- "input",
149
- "name":
150
- "request",
151
- "message":
152
- f"What is the request you want to answer? (Enter the coded request as integer in hexadecimal format (e.g. 12 3B 5)",
153
- "filter":
154
- lambda input: _convert_string_to_bytes(input),
155
- }]
156
- answer = IP_prompt(answered_request_prompt)
157
- answered_request = cast(bytes, answer.get("request"))
158
- print(f"Input interpretation as list: {list(answered_request)}")
159
-
160
- # Request values for parameters
161
- for key, param_or_structure in param_dict.items():
162
- if isinstance(param_or_structure, dict):
163
- # param_or_structure refers to a structure (represented as dict of params)
166
+ # Query user for the values of all settable parameters
167
+ for param in codec.parameters:
168
+ if (inner_params := getattr(dop := getattr(param, "dop", None), "parameters",
169
+ None)) is not None:
170
+ assert isinstance(dop, DopBase)
171
+ inner_params = cast(List[Parameter], inner_params)
172
+ # param refers to a complex DOP, i.e., the required
173
+ # value is a key-value dict
164
174
  print(
165
- f"The next {len(param_or_structure)} parameters belong to the structure '{key}'"
175
+ f"The next {len(inner_params)} parameters belong to the structure '{dop.short_name}'"
166
176
  )
167
177
  structure_param_values: ParameterValueDict = {}
168
- for param_sn, param in param_or_structure.items():
169
- if not isinstance(param, dict) and param.is_settable:
170
- val = prompt_single_parameter_value(param)
178
+ for inner_param in inner_params:
179
+ if inner_param.is_settable:
180
+ val = prompt_single_parameter_value(inner_param)
171
181
  if val is not None:
172
- structure_param_values[param_sn] = val
173
- param_values[key] = structure_param_values
174
- elif param_or_structure.is_settable:
175
- # param_or_structure is a parameter
176
- val = prompt_single_parameter_value(param_or_structure)
182
+ structure_param_values[inner_param.short_name] = val
183
+ param_values[param.short_name] = structure_param_values
184
+ elif param.is_settable:
185
+ val = prompt_single_parameter_value(param)
177
186
  if val is not None:
178
- param_values[key] = val
179
- if isinstance(sub_service, Response):
180
- payload = sub_service.encode(coded_request=answered_request, **param_values)
187
+ param_values[param.short_name] = val
188
+
189
+ if isinstance(codec, Response):
190
+ payload = codec.encode(coded_request=answered_request, **param_values)
181
191
  else:
182
- payload = sub_service.encode(coded_request=b'', **param_values)
192
+ payload = codec.encode(coded_request=b'', **param_values)
183
193
  else:
184
- # There are no optional parameters that need to be defined by the user -> Just print message
185
- payload = sub_service.encode()
194
+ # There are no settable parameters -> Just print message
195
+ if isinstance(codec, Response):
196
+ payload = codec.encode(coded_request=answered_request)
197
+ else:
198
+ payload = codec.encode()
199
+
186
200
  print(f"Message payload: 0x{bytes(payload).hex()}")
187
201
 
188
202
 
@@ -193,56 +207,58 @@ def encode_message_from_string_values(
193
207
  if parameter_values is None:
194
208
  parameter_values = {}
195
209
  parameter_values = parameter_values.copy()
196
- param_dict = sub_service.parameter_dict()
197
210
 
198
- # Check if all needed parameters are given
211
+ # Check if all needed parameters have been specified
199
212
  missing_parameter_names = []
200
- for param_sn, param in param_dict.items():
201
- if isinstance(param, dict):
202
- # param_value refers to a structure (represented as dict of params)
203
- for simple_param_sn, simple_param in param.items():
204
- structured_value = parameter_values.get(param_sn)
205
- if not isinstance(simple_param, Parameter):
206
- continue
207
- if simple_param.is_required and (not isinstance(structured_value, dict) or
208
- structured_value.get(simple_param_sn) is None):
209
- missing_parameter_names.append(f"{param_sn} :: {simple_param_sn}")
213
+ for param in sub_service.parameters:
214
+ if (inner_params := getattr(dop := getattr(param, "dop", None), "parameters",
215
+ None)) is not None:
216
+ inner_param_values = parameter_values.get(param.short_name, {})
217
+ if not isinstance(inner_param_values, dict):
218
+ print(f"Value for composite parameter {param.short_name} must be "
219
+ f"a dictionary, got {type(inner_param_values).__name__}")
220
+ continue
221
+ for inner_param in inner_params:
222
+ if inner_param.is_required and inner_param.short_name not in inner_param_values:
223
+ missing_parameter_names.append(f"{param.short_name}.{inner_param.short_name}")
210
224
  else:
211
- if param.is_required and parameter_values.get(param_sn) is None:
212
- missing_parameter_names.append(param_sn)
225
+ if param.is_required and parameter_values.get(param.short_name) is None:
226
+ missing_parameter_names.append(param.short_name)
213
227
 
214
228
  if len(missing_parameter_names) > 0:
215
- print("The following parameters are required but missing!")
216
- print(" - " + "\n - ".join(missing_parameter_names))
229
+ print("The following parameters are required but missing:")
230
+ print(" - " + "\n - ".join(sorted(missing_parameter_names)))
217
231
  return
218
232
 
219
233
  # Request values for parameters
220
234
  for parameter_sn, parameter_value in parameter_values.items():
221
- parameter = param_dict.get(parameter_sn)
235
+ parameter = resolve_snref(parameter_sn, sub_service.parameters, Parameter)
222
236
  if parameter is None:
223
237
  print(f"I don't know the parameter {parameter_sn}")
224
238
  continue
225
239
 
226
240
  if isinstance(parameter_value, dict):
227
241
  # parameter_value refers to a structure (represented as dict of params)
242
+ dop = getattr(parameter, "dop", None)
243
+ inner_params = getattr(dop, "parameters", None)
244
+ assert isinstance(dop, ComplexDop)
245
+ assert isinstance(inner_params, list)
246
+ inner_params = cast(List[Parameter], inner_params)
247
+
228
248
  typed_dict = parameter_value.copy()
229
- for simple_param_sn, simple_param_val in parameter_value.items():
230
- simple_parameter = param_dict.get(simple_param_sn)
231
- if simple_parameter is None:
232
- print(f"Unknown sub-parameter {simple_param_sn}")
249
+ for inner_param_sn, inner_param_value in parameter_value.items():
250
+ inner_param = resolve_snref(inner_param_sn, inner_params, Parameter)
251
+ if inner_param is None:
252
+ print(f"Unknown sub-parameter {inner_param_sn}")
233
253
  continue
234
- if not isinstance(simple_param_val, str):
235
- print(f"The value specified for parameter {simple_param_sn} is not a string")
254
+ if not isinstance(inner_param_value, str):
255
+ print(f"The value specified for parameter {inner_param_sn} is not a string")
236
256
  continue
237
257
 
238
- typed_dict[simple_param_sn] = _convert_string_to_odx_type(
239
- simple_param_val,
240
- simple_parameter.physical_type.base_data_type # type: ignore[union-attr]
241
- )
242
- parameter_values[parameter_sn] = typed_dict
258
+ typed_dict[inner_param_sn] = _convert_string_to_odx_type(
259
+ inner_param_value, inner_param.physical_type.base_data_type)
260
+ parameter_values[parameter.short_name] = typed_dict
243
261
  else:
244
- assert isinstance(parameter, Parameter)
245
-
246
262
  if not isinstance(parameter_value, str):
247
263
  print(f"Value for parameter {parameter_sn} is not a string")
248
264
  continue
@@ -356,8 +372,7 @@ def browse(odxdb: Database) -> None:
356
372
  if codec is not None:
357
373
  assert isinstance(codec, (Request, Response))
358
374
  table = extract_parameter_tabulation_data(codec.parameters)
359
- table_str = tabulate(table, headers='keys', tablefmt='presto')
360
- print(table_str)
375
+ print(table)
361
376
 
362
377
  encode_message_interactively(codec, ask_user_confirmation=True)
363
378