fameio 3.1.1__py3-none-any.whl → 3.3.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 (57) hide show
  1. fameio/cli/convert_results.py +10 -10
  2. fameio/cli/make_config.py +9 -9
  3. fameio/cli/options.py +6 -4
  4. fameio/cli/parser.py +87 -51
  5. fameio/cli/reformat.py +58 -0
  6. fameio/input/__init__.py +4 -4
  7. fameio/input/loader/__init__.py +13 -13
  8. fameio/input/loader/controller.py +64 -18
  9. fameio/input/loader/loader.py +25 -16
  10. fameio/input/metadata.py +57 -38
  11. fameio/input/resolver.py +9 -10
  12. fameio/input/scenario/agent.py +62 -26
  13. fameio/input/scenario/attribute.py +93 -40
  14. fameio/input/scenario/contract.py +160 -56
  15. fameio/input/scenario/exception.py +41 -18
  16. fameio/input/scenario/fameiofactory.py +57 -6
  17. fameio/input/scenario/generalproperties.py +22 -12
  18. fameio/input/scenario/scenario.py +117 -38
  19. fameio/input/scenario/stringset.py +29 -11
  20. fameio/input/schema/agenttype.py +27 -10
  21. fameio/input/schema/attribute.py +108 -45
  22. fameio/input/schema/java_packages.py +14 -12
  23. fameio/input/schema/schema.py +39 -15
  24. fameio/input/validator.py +198 -54
  25. fameio/input/writer.py +137 -46
  26. fameio/logs.py +28 -47
  27. fameio/output/__init__.py +5 -1
  28. fameio/output/agent_type.py +89 -28
  29. fameio/output/conversion.py +52 -37
  30. fameio/output/csv_writer.py +107 -27
  31. fameio/output/data_transformer.py +17 -24
  32. fameio/output/execution_dao.py +170 -0
  33. fameio/output/input_dao.py +71 -33
  34. fameio/output/output_dao.py +33 -11
  35. fameio/output/reader.py +64 -21
  36. fameio/output/yaml_writer.py +16 -8
  37. fameio/scripts/__init__.py +22 -4
  38. fameio/scripts/convert_results.py +126 -52
  39. fameio/scripts/convert_results.py.license +1 -1
  40. fameio/scripts/exception.py +7 -0
  41. fameio/scripts/make_config.py +34 -13
  42. fameio/scripts/make_config.py.license +1 -1
  43. fameio/scripts/reformat.py +71 -0
  44. fameio/scripts/reformat.py.license +3 -0
  45. fameio/series.py +174 -59
  46. fameio/time.py +79 -25
  47. fameio/tools.py +48 -8
  48. {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/METADATA +50 -34
  49. fameio-3.3.0.dist-info/RECORD +60 -0
  50. {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/WHEEL +1 -1
  51. {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/entry_points.txt +1 -0
  52. CHANGELOG.md +0 -288
  53. fameio-3.1.1.dist-info/RECORD +0 -56
  54. {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSE.txt +0 -0
  55. {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  56. {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
  57. {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
fameio/input/validator.py CHANGED
@@ -1,10 +1,13 @@
1
1
  # SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
4
6
  import math
5
7
  from collections import Counter
6
- from typing import Any, Union
8
+ from typing import Any
7
9
 
10
+ from fameio.input import InputError
8
11
  from fameio.input.resolver import PathResolver
9
12
  from fameio.input.scenario import Agent, Attribute, Contract, Scenario, StringSet
10
13
  from fameio.input.schema import Schema, AttributeSpecs, AttributeType, AgentType
@@ -13,12 +16,12 @@ from fameio.series import TimeSeriesManager, TimeSeriesError
13
16
  from fameio.time import FameTime
14
17
 
15
18
 
16
- class ValidationError(Exception):
17
- """Indicates an error occurred during validation of any data with a connected schema"""
19
+ class ValidationError(InputError):
20
+ """Indicates an error occurred during validation of any data with a connected schema."""
18
21
 
19
22
 
20
23
  class SchemaValidator:
21
- """Handles validation of scenarios based on a connected `schema`"""
24
+ """Handles validation of scenarios based on a connected `schema`."""
22
25
 
23
26
  _AGENT_ID_NOT_UNIQUE = "Agent ID(s) not unique: '{}'."
24
27
  _AGENT_TYPE_UNKNOWN = "Agent type '{}' not declared in Schema."
@@ -26,8 +29,8 @@ class SchemaValidator:
26
29
  _TYPE_NOT_IMPLEMENTED = "Validation not implemented for AttributeType '{}'."
27
30
  _INCOMPATIBLE = "Value '{}' incompatible with {} of Attribute '{}'."
28
31
  _DISALLOWED = "Value '{}' not in list of allowed values of Attribute '{}'"
29
- _AGENT_MISSING = "Contract mentions Agent with ID '{}' but Agent was not declared."
30
- _PRODUCT_MISSING = "Product '{}' not declared in Schema for AgentType '{}'."
32
+ _AGENT_MISSING = "Agent with ID '{}' was not declared in Scenario but used in Contract: '{}'"
33
+ _PRODUCT_MISSING = "'{}' is no product of AgentType '{}'. Contract invalid: '{}'"
31
34
  _KEY_MISSING = "Required key '{}' missing in dictionary '{}'."
32
35
  _ATTRIBUTE_MISSING = "Mandatory attribute '{}' is missing."
33
36
  _DEFAULT_IGNORED = "Optional Attribute: '{}': not specified - provided Default ignored for optional Attributes."
@@ -42,9 +45,7 @@ class SchemaValidator:
42
45
  def validate_scenario_and_timeseries(
43
46
  scenario: Scenario, path_resolver: PathResolver = PathResolver()
44
47
  ) -> TimeSeriesManager:
45
- """
46
- Validates the given `scenario` and its timeseries using given `path_resolver`
47
- Raises an exception if schema requirements are not met or timeseries data are erroneous.
48
+ """Validates the given `scenario` and its timeseries using given `path_resolver`.
48
49
 
49
50
  Args:
50
51
  scenario: to be validated against the encompassed schema
@@ -52,8 +53,9 @@ class SchemaValidator:
52
53
 
53
54
  Returns:
54
55
  a new TimeSeriesManager initialised with validated time series from scenario
56
+
55
57
  Raises:
56
- ValidationException: if an error in the scenario or in timeseries is spotted
58
+ ValidationError: if schema requirements are not met or timeseries are erroneous, logged with level "ERROR"
57
59
  """
58
60
  schema = scenario.schema
59
61
  agents = scenario.agents
@@ -72,28 +74,63 @@ class SchemaValidator:
72
74
 
73
75
  @staticmethod
74
76
  def ensure_unique_agent_ids(agents: list[Agent]) -> None:
75
- """Raises exception if any id for given `agents` is not unique"""
77
+ """Ensures that IDs of given agents are unique.
78
+
79
+ Args:
80
+ agents: whose IDs are to be checked to uniqueness
81
+
82
+ Raises:
83
+ ValidationError: if any id for given `agents` is not unique, logged with level "ERROR"
84
+ """
76
85
  list_of_ids = [agent.id for agent in agents]
77
86
  non_unique_ids = [agent_id for agent_id, count in Counter(list_of_ids).items() if count > 1]
78
87
  if non_unique_ids:
79
88
  raise log_error(ValidationError(SchemaValidator._AGENT_ID_NOT_UNIQUE.format(non_unique_ids)))
80
89
 
81
90
  @staticmethod
82
- def ensure_agent_and_timeseries_are_valid(agent: Agent, schema: Schema, timeseries_manager: TimeSeriesManager):
83
- """Validates given `agent` against `schema` plus loads and validates its timeseries"""
91
+ def ensure_agent_and_timeseries_are_valid(
92
+ agent: Agent, schema: Schema, timeseries_manager: TimeSeriesManager
93
+ ) -> None:
94
+ """Validates given `agent` against `schema`, loads and validates its timeseries.
95
+
96
+ Args:
97
+ agent: to be checked
98
+ schema: to check the agent against
99
+ timeseries_manager: to register new timeseries at
100
+
101
+ Raises:
102
+ ValidationError: if agent is not in schema, has missing or invalid data; logged with level "ERROR"
103
+ """
84
104
  SchemaValidator.ensure_agent_type_in_schema(agent, schema)
85
105
  SchemaValidator.ensure_is_valid_agent(agent, schema, timeseries_manager)
86
106
  SchemaValidator.load_and_validate_timeseries(agent, schema, timeseries_manager)
87
107
 
88
108
  @staticmethod
89
109
  def ensure_agent_type_in_schema(agent: Agent, schema: Schema) -> None:
90
- """Raises exception if type for given `agent` is not specified in given `schema`"""
110
+ """Makes sure that the given agent is contained in the given schema.
111
+
112
+ Args:
113
+ agent: to be checked
114
+ schema: that ought to contain the agent
115
+
116
+ Raises:
117
+ ValidationError: if type for given `agent` is not specified in given `schema`, logged with level "ERROR"
118
+ """
91
119
  if agent.type_name not in schema.agent_types:
92
120
  raise log_error(ValidationError(SchemaValidator._AGENT_TYPE_UNKNOWN.format(agent.type_name)))
93
121
 
94
122
  @staticmethod
95
123
  def ensure_is_valid_agent(agent: Agent, schema: Schema, timeseries_manager: TimeSeriesManager) -> None:
96
- """Raises an exception if given `agent` does not meet the specified `schema` requirements"""
124
+ """Ensures that given `agent` meets the specified `schema` requirements and registers new timeseries
125
+
126
+ Args:
127
+ agent: to be checked
128
+ schema: to check against
129
+ timeseries_manager: to register new timeseries at
130
+
131
+ Raises:
132
+ ValidationError: if the agent doesn't meet the schema's requirements, logged with level "ERROR"
133
+ """
97
134
  scenario_attributes = agent.attributes
98
135
  schema_attributes = SchemaValidator._get_agent(schema, agent.type_name).attributes
99
136
  missing_default_series = SchemaValidator._check_mandatory_or_default(scenario_attributes, schema_attributes)
@@ -104,7 +141,18 @@ class SchemaValidator:
104
141
 
105
142
  @staticmethod
106
143
  def _get_agent(schema: Schema, name: str) -> AgentType:
107
- """Returns agent specified by `name` or raises Exception if this agent is not present in given `schema`"""
144
+ """Returns agent type as specified by `name`.
145
+
146
+ Args:
147
+ schema: to obtain the agent type from
148
+ name: of the agent type to obtain
149
+
150
+ Returns:
151
+ AgentType corresponding to given name
152
+
153
+ Raises:
154
+ ValidationError: if this agent is not present in given `schema`, logged with level "ERROR"
155
+ """
108
156
  if name in schema.agent_types:
109
157
  return schema.agent_types[name]
110
158
  raise log_error(ValidationError(SchemaValidator._AGENT_TYPE_UNKNOWN.format(name)))
@@ -113,15 +161,22 @@ class SchemaValidator:
113
161
  def _check_mandatory_or_default(
114
162
  attributes: dict[str, Attribute],
115
163
  specifications: dict[str, AttributeSpecs],
116
- ) -> list[Union[str, float]]:
117
- """
118
- Raises Exception if in given list of `specifications` at least one item is mandatory,
119
- provides no defaults and is not contained in given `attributes` dictionary
164
+ ) -> list[str | float]:
165
+ """Ensures that each attribute that is mandatory has either a value specified or a default value available.
166
+
167
+ Also gathers and returns all default values of time series attributes.
168
+
169
+ Args:
170
+ attributes: to check for completeness
171
+ specifications: to check attributes against
120
172
 
121
173
  Returns:
122
174
  list of time series defaults used in scenario
175
+
176
+ Raises:
177
+ ValidationError: if any mandatory attribute is missing and has no default
123
178
  """
124
- missing_series_defaults = []
179
+ missing_series_defaults: list[str | float] = []
125
180
  for name, specification in specifications.items():
126
181
  if name not in attributes:
127
182
  if specification.is_mandatory:
@@ -130,7 +185,7 @@ class SchemaValidator:
130
185
  ValidationError(SchemaValidator._ATTRIBUTE_MISSING.format(specification.full_name))
131
186
  )
132
187
  if specification.attr_type == AttributeType.TIME_SERIES:
133
- missing_series_defaults.append(specification.default_value)
188
+ missing_series_defaults.append(specification.default_value) # type: ignore[arg-type]
134
189
  else:
135
190
  if specification.has_default_value:
136
191
  log().warning(SchemaValidator._DEFAULT_IGNORED.format(specification.full_name))
@@ -151,7 +206,16 @@ class SchemaValidator:
151
206
 
152
207
  @staticmethod
153
208
  def _ensure_attributes_exist(attributes: dict[str, Attribute], specifications: dict[str, AttributeSpecs]) -> None:
154
- """Raises exception any entry of given `attributes` has no corresponding type `specification`"""
209
+ """Ensures that each attribute has a corresponding entry in given specifications.
210
+
211
+ Args:
212
+ attributes: to search specifications for
213
+ specifications: describing the attributes
214
+
215
+ Raises:
216
+ ValidationError: if any entry of given `attributes` has no corresponding type `specification`,
217
+ logged with level "ERROR"
218
+ """
155
219
  for name, attribute in attributes.items():
156
220
  if name not in specifications:
157
221
  raise log_error(ValidationError(SchemaValidator._ATTRIBUTE_UNKNOWN.format(attribute)))
@@ -167,7 +231,16 @@ class SchemaValidator:
167
231
  def _ensure_value_and_type_match(
168
232
  attributes: dict[str, Attribute], specifications: dict[str, AttributeSpecs]
169
233
  ) -> None:
170
- """Raises exception if in given list of `attributes` its value does not match associated type `specification`"""
234
+ """Ensure that the value of an attribute match the attribute's type and are allowed.
235
+
236
+ Args:
237
+ attributes: to check the values for
238
+ specifications: describing the attribute (and potential value restrictions)
239
+
240
+ Raises:
241
+ ValidationError: if in given list of `attributes` any value does not match associated type `specification`,
242
+ logged with level "ERROR"
243
+ """
171
244
  for name, attribute in attributes.items():
172
245
  specification = specifications[name]
173
246
  if attribute.has_value:
@@ -187,7 +260,18 @@ class SchemaValidator:
187
260
 
188
261
  @staticmethod
189
262
  def _is_compatible(specification: AttributeSpecs, value_or_values: Any) -> bool:
190
- """Returns True if given `value_or_values` is compatible to specified `attribute_type` and `should_be_list`"""
263
+ """Checks if given `value_or_values` is compatible with the given `specification`.
264
+
265
+ Args:
266
+ specification: of the attribute for which to check the values
267
+ value_or_values: singe value or list of values that is to be checked for compatibility
268
+
269
+ Returns:
270
+ True if given `value_or_values` is compatible the to specified `attribute_type`, False otherwise
271
+
272
+ Raises:
273
+ ValidationError: if an unknown attribute type is encountered, logged with level "ERROR"
274
+ """
191
275
  is_list = isinstance(value_or_values, list)
192
276
  attribute_type = specification.attr_type
193
277
  if specification.is_list:
@@ -202,7 +286,19 @@ class SchemaValidator:
202
286
 
203
287
  @staticmethod
204
288
  def _is_compatible_value(attribute_type: AttributeType, value) -> bool:
205
- """Returns True if given single value is compatible to specified `attribute_type` and is not a NaN float"""
289
+ """Checks if given value is compatible with the specifications of the `attribute_type`.
290
+
291
+ Args:
292
+ attribute_type: specification to test the value against
293
+ value: to be tested for compatibility
294
+
295
+ Returns:
296
+ True if given single value is compatible to specified `attribute_type` and is not a NaN float,
297
+ False otherwise
298
+
299
+ Raises:
300
+ ValidationError: if checks for the attribute type are not implemented, logged with level "ERROR"
301
+ """
206
302
  if attribute_type is AttributeType.INTEGER:
207
303
  if isinstance(value, int):
208
304
  return -2147483648 < value < 2147483647
@@ -221,15 +317,20 @@ class SchemaValidator:
221
317
 
222
318
  @staticmethod
223
319
  def _is_allowed_value(attribute: AttributeSpecs, value) -> bool:
224
- """Returns True if `value` matches an entry of given `Attribute`'s value list or if this list is empty"""
225
- if not attribute.values:
226
- return True
227
- return value in attribute.values
320
+ """Checks if given value is on the list of allowed values for an attribute type.
321
+
322
+ Args:
323
+ attribute: type description of an attribute
324
+ value: to be checked if compatible with the attribute type's value restrictions
325
+
326
+ Returns:
327
+ True if `value` matches an entry of given `Attribute`'s value list or if this list is empty
328
+ """
329
+ return not attribute.values or value in attribute.values
228
330
 
229
331
  @staticmethod
230
332
  def load_and_validate_timeseries(agent: Agent, schema: Schema, timeseries_manager: TimeSeriesManager) -> None:
231
- """
232
- Loads all timeseries specified in given `schema` of given `agent` into given `timeseries_manager`
333
+ """Loads all timeseries in given `schema` for given `agent`. Uses `timeseries_manager` to validates them.
233
334
 
234
335
  Args:
235
336
  agent: definition in scenario
@@ -237,39 +338,55 @@ class SchemaValidator:
237
338
  timeseries_manager: to be filled with timeseries
238
339
 
239
340
  Raises:
240
- ValidationException: if timeseries is not found, ill-formatted or invalid
341
+ ValidationError: if timeseries is not found, ill-formatted or invalid
241
342
  """
242
343
  scenario_attributes = agent.attributes
243
344
  schema_attributes = SchemaValidator._get_agent(schema, agent.type_name).attributes
244
- SchemaValidator._ensure_valid_timeseries(scenario_attributes, schema_attributes, timeseries_manager)
345
+ SchemaValidator._register_timeseries(scenario_attributes, schema_attributes, timeseries_manager)
245
346
 
246
347
  @staticmethod
247
- def _ensure_valid_timeseries(
348
+ def _register_timeseries(
248
349
  attributes: dict[str, Attribute], specifications: dict[str, AttributeSpecs], manager: TimeSeriesManager
249
350
  ) -> None:
250
- """Recursively searches for time_series in agent attributes and registers them at given `manager`"""
351
+ """Recursively searches for timeseries in agent attributes and registers them at given `manager`.
352
+
353
+ Args:
354
+ attributes: to search timeseries in
355
+ specifications: corresponding to the attributes
356
+ manager: to register new timeseries at
357
+
358
+ Raises:
359
+ ValidationError: if a timeseries could not be registered, logged at level "ERROR"
360
+ """
251
361
  for name, attribute in attributes.items():
252
362
  specification = specifications[name]
253
363
  if attribute.has_value:
254
364
  attribute_type = specification.attr_type
255
365
  if attribute_type is AttributeType.TIME_SERIES:
256
366
  try:
257
- manager.register_and_validate(attribute.value)
367
+ manager.register_and_validate(attribute.value) # type: ignore[arg-type]
258
368
  except TimeSeriesError as e:
259
369
  message = SchemaValidator._TIME_SERIES_INVALID.format(specification.full_name)
260
- raise log_error(ValidationError(message, e)) from e
370
+ raise log_error(ValidationError(message)) from e
261
371
  if attribute.has_nested:
262
- SchemaValidator._ensure_valid_timeseries(attribute.nested, specification.nested_attributes, manager)
372
+ SchemaValidator._register_timeseries(attribute.nested, specification.nested_attributes, manager)
263
373
  if attribute.has_nested_list:
264
374
  for entry in attribute.nested_list:
265
- SchemaValidator._ensure_valid_timeseries(entry, specification.nested_attributes, manager)
375
+ SchemaValidator._register_timeseries(entry, specification.nested_attributes, manager)
266
376
 
267
377
  @staticmethod
268
378
  def ensure_string_set_consistency(agent: Agent, schema: Schema, string_sets: dict[str, StringSet]) -> None:
269
- """
270
- Raises exception if
271
- a) an agent's attribute is of type StringSet but the corresponding StringSet is not defined in the scenario
272
- b) the value assigned to an attribute of type StringSet is not contained in the corresponding StringSet
379
+ """Checks consistency of an `agent's` StringSet attributes as mentioned in `schema` with provided `string_sets`.
380
+
381
+ Args:
382
+ agent: whose StringSet attributes are to be checked for consistency
383
+ schema: describing the agent's attributes
384
+ string_sets: as defined in the scenario and to test the agents attribute against
385
+
386
+ Raises:
387
+ ValidationError: logged with level "ERROR", occur when either
388
+ a) an agent's attribute is type StringSet but the corresponding StringSet is not defined in the scenario, or
389
+ b) the value assigned to an attribute of type StringSet is not contained in the corresponding StringSet
273
390
  """
274
391
  scenario_attributes = agent.attributes
275
392
  schema_attributes = SchemaValidator._get_agent(schema, agent.type_name).attributes
@@ -279,12 +396,19 @@ class SchemaValidator:
279
396
  def _ensure_string_set_consistency(
280
397
  attributes: dict[str, Attribute], specifications: dict[str, AttributeSpecs], string_sets: dict[str, StringSet]
281
398
  ) -> None:
282
- """
283
- Recursively iterates through all attributes of an agent, applying tests if attribute type is `StringSet`
399
+ """Recursively iterates through all attributes of an agent checking consistency of `StringSet` type attributes.
400
+
401
+ Checks consistency of agent `StringSet` attributes with provided `string_sets` in the scenario and schema.
402
+
403
+ Args:
404
+ attributes: attributes of an agent
405
+ specifications: corresponding to the provided attributes
406
+ string_sets: to check attributes of type string_set against
407
+
284
408
  Raises:
285
- ValidationException: if
286
- a) StringSet mentioned in schema is not defined in the scenario
287
- b) the value assigned to an attribute of type StringSet is not contained in the corresponding StringSet
409
+ ValidationError: logged with level "ERROR", occur when
410
+ a) StringSet declared in schema is not defined in the section "StringSet" in the scenario, or
411
+ b) value assigned to an attribute of type StringSet is not contained in the corresponding StringSet
288
412
  """
289
413
  for name, attribute in attributes.items():
290
414
  specification = specifications[name]
@@ -310,23 +434,43 @@ class SchemaValidator:
310
434
 
311
435
  @staticmethod
312
436
  def ensure_is_valid_contract(contract: Contract, schema: Schema, agent_types_by_id: dict[int, str]) -> None:
313
- """Raises exception if given `contract` does not meet the `schema`'s requirements, using `agent_types_by_id`"""
437
+ """Checks validity of a contract's IDs and product.
438
+
439
+ Ensures that for a given `contract` sender and receiver IDs are valid, and that the sender offers the
440
+ contracted product.
441
+
442
+ Args:
443
+ contract: to be checked
444
+ schema: to extract the sender's available products from
445
+ agent_types_by_id: to test if sender and receiver IDs are contained
446
+
447
+ Raises:
448
+ ValidationError: if given `contract` uses unknown agent IDs or an unknown product, logged with level "ERROR"
449
+ """
314
450
  sender_id = contract.sender_id
315
451
  if sender_id not in agent_types_by_id:
316
- raise log_error(ValidationError(SchemaValidator._AGENT_MISSING.format(sender_id)))
452
+ raise log_error(ValidationError(SchemaValidator._AGENT_MISSING.format(sender_id, contract.to_dict())))
317
453
  if contract.receiver_id not in agent_types_by_id:
318
- raise log_error(ValidationError(SchemaValidator._AGENT_MISSING.format(contract.receiver_id)))
454
+ raise log_error(
455
+ ValidationError(SchemaValidator._AGENT_MISSING.format(contract.receiver_id, contract.to_dict()))
456
+ )
319
457
  sender_type_name = agent_types_by_id[sender_id]
320
458
  if sender_type_name not in schema.agent_types:
321
459
  raise log_error(ValidationError(SchemaValidator._AGENT_TYPE_UNKNOWN.format(sender_type_name)))
322
460
  sender_type = schema.agent_types[sender_type_name]
323
461
  product = contract.product_name
324
462
  if product not in sender_type.products:
325
- raise log_error(ValidationError(SchemaValidator._PRODUCT_MISSING.format(product, sender_type_name)))
463
+ raise log_error(
464
+ ValidationError(SchemaValidator._PRODUCT_MISSING.format(product, sender_type_name, contract.to_dict()))
465
+ )
326
466
 
327
467
  @staticmethod
328
468
  def check_agents_have_contracts(scenario: Scenario) -> None:
329
- """Raises warning for each agent without any assigned contract"""
469
+ """Loads a warning for each agent without any assigned contract.
470
+
471
+ Args:
472
+ scenario: to search for agents without any contract
473
+ """
330
474
  senders = [contract.sender_id for contract in scenario.contracts]
331
475
  receivers = [contract.receiver_id for contract in scenario.contracts]
332
476
  active_agents = set(senders + receivers)