fameio 3.3.0__tar.gz → 3.4.0__tar.gz

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 (60) hide show
  1. {fameio-3.3.0 → fameio-3.4.0}/CHANGELOG.md +13 -1
  2. {fameio-3.3.0 → fameio-3.4.0}/PKG-INFO +32 -18
  3. {fameio-3.3.0 → fameio-3.4.0}/README.md +31 -17
  4. {fameio-3.3.0 → fameio-3.4.0}/pyproject.toml +1 -1
  5. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/contract.py +55 -10
  6. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/time.py +41 -0
  7. {fameio-3.3.0 → fameio-3.4.0}/LICENSE.txt +0 -0
  8. {fameio-3.3.0 → fameio-3.4.0}/LICENSES/Apache-2.0.txt +0 -0
  9. {fameio-3.3.0 → fameio-3.4.0}/LICENSES/CC-BY-4.0.txt +0 -0
  10. {fameio-3.3.0 → fameio-3.4.0}/LICENSES/CC0-1.0.txt +0 -0
  11. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/__init__.py +0 -0
  12. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/cli/__init__.py +0 -0
  13. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/cli/convert_results.py +0 -0
  14. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/cli/make_config.py +0 -0
  15. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/cli/options.py +0 -0
  16. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/cli/parser.py +0 -0
  17. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/cli/reformat.py +0 -0
  18. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/__init__.py +0 -0
  19. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/loader/__init__.py +0 -0
  20. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/loader/controller.py +0 -0
  21. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/loader/loader.py +0 -0
  22. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/metadata.py +0 -0
  23. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/resolver.py +0 -0
  24. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/__init__.py +0 -0
  25. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/agent.py +0 -0
  26. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/attribute.py +0 -0
  27. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/exception.py +0 -0
  28. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/fameiofactory.py +0 -0
  29. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/generalproperties.py +0 -0
  30. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/scenario.py +0 -0
  31. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/scenario/stringset.py +0 -0
  32. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/schema/__init__.py +0 -0
  33. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/schema/agenttype.py +0 -0
  34. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/schema/attribute.py +0 -0
  35. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/schema/java_packages.py +0 -0
  36. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/schema/schema.py +0 -0
  37. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/validator.py +0 -0
  38. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/input/writer.py +0 -0
  39. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/logs.py +0 -0
  40. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/__init__.py +0 -0
  41. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/agent_type.py +0 -0
  42. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/conversion.py +0 -0
  43. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/csv_writer.py +0 -0
  44. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/data_transformer.py +0 -0
  45. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/execution_dao.py +0 -0
  46. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/input_dao.py +0 -0
  47. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/output_dao.py +0 -0
  48. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/reader.py +0 -0
  49. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/output/yaml_writer.py +0 -0
  50. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/__init__.py +0 -0
  51. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/__init__.py.license +0 -0
  52. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/convert_results.py +0 -0
  53. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/convert_results.py.license +0 -0
  54. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/exception.py +0 -0
  55. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/make_config.py +0 -0
  56. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/make_config.py.license +0 -0
  57. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/reformat.py +0 -0
  58. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/scripts/reformat.py.license +0 -0
  59. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/series.py +0 -0
  60. {fameio-3.3.0 → fameio-3.4.0}/src/fameio/tools.py +0 -0
@@ -2,7 +2,19 @@
2
2
 
3
3
  SPDX-License-Identifier: CC0-1.0 -->
4
4
 
5
- ## [3.3.0](https://gitlab.com/fame-framework/fame-io/-/tags/v3.3.0) - 2025-5-09
5
+ ## [3.4.0](https://gitlab.com/fame-framework/fame-io/-/tags/v3.4.0) - 2025-05-27
6
+ ### Changed
7
+ - Allow nesting of sender or receiver lists in contracts !228 (@dlr-cjs)
8
+
9
+ ### Added
10
+ - Add new keyword "Every" to Contracts that allow text qualification of Contract duration #249 (@dlr-cjs)
11
+ - Add helper method to return first time stamp of a given year #247 (@dlr-cjs)
12
+ - Add checks for Python 3.13 to CI #250 (@dlr-cjs)
13
+
14
+ ### Fixed
15
+ - Avoid causing a traceback by nested contract sender or receiver lists #228 (@dlr-cjs)
16
+
17
+ ## [3.3.0](https://gitlab.com/fame-framework/fame-io/-/tags/v3.3.0) - 2025-05-09
6
18
  ### Changed
7
19
  - Expose static methods to read, convert, and write time series #245 (@dlr-cjs)
8
20
  - Improve docstrings of SchemaValidator !219 (@dlr-cjs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fameio
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: Tools for input preparation and output digestion of FAME models
5
5
  License: Apache-2.0
6
6
  Keywords: FAME,fameio,agent-based modelling,energy systems
@@ -507,7 +507,7 @@ Contracts:
507
507
  ReceiverId: 2
508
508
  ProductName: ProductOfAgent_1
509
509
  FirstDeliveryTime: -25
510
- DeliveryIntervalInSteps: 3600
510
+ Every: 3600
511
511
  Metadata:
512
512
  Some: "additional information can go here"
513
513
 
@@ -515,7 +515,7 @@ Contracts:
515
515
  ReceiverId: 1
516
516
  ProductName: ProductOfAgent_2
517
517
  FirstDeliveryTime: -22
518
- DeliveryIntervalInSteps: 3600
518
+ Every: 1 hour
519
519
  Attributes:
520
520
  ProductAppendix: value
521
521
  TimeOffset: 42
@@ -527,7 +527,8 @@ Contract Parameters:
527
527
  * `ReceiverId` unique ID of agent receiving the product
528
528
  * `ProductName` name of the product to be sent
529
529
  * `FirstDeliveryTime` first time of delivery in the format "seconds after the January 1st 2000, 00:00:00"
530
- * `DeliveryIntervalInSteps` delay time in between deliveries in seconds
530
+ * `Every` delay time in between deliveries; either an integer value in seconds, or a qualified time span in the format "<integer> <TimeUnit>(s)", where TimeUnit is one of "second", "minute", "hour", "day", "week", "month", "year" - with an options "s" at the end; mind that week, month, and year refer to fixed-length intervals of 168, 730, and 8760 hours.
531
+ * `DeliveryIntervalInSteps` deprecated; delay time in between deliveries in seconds (use instead of `Every`)
531
532
  * `Metadata` can be assigned to add further helpful information about a Contract
532
533
  * `Attributes` can be set to include additional information as `int`, `float`, `enum`, or `dict` data types
533
534
 
@@ -541,59 +542,72 @@ example:
541
542
  ```yaml
542
543
  Contracts:
543
544
  # effectively 3 similar contracts (0 -> 11), (0 -> 12), (0 -> 13)
544
- # with otherwise identical ProductName, FirstDeliveryTime & DeliveryIntervalInSteps
545
+ # with otherwise identical ProductName, FirstDeliveryTime, and Every
545
546
  - SenderId: 0
546
547
  ReceiverId: [ 11, 12, 13 ]
547
548
  ProductName: MyOtherProduct
548
549
  FirstDeliveryTime: 100
549
- DeliveryIntervalInSteps: 3600
550
+ Every: 1 hour
550
551
 
551
552
  # effectively 3 similar contracts (1 -> 10), (2 -> 10), (3 -> 10)
552
- # with otherwise identical ProductName, FirstDeliveryTime & DeliveryIntervalInSteps
553
+ # with otherwise identical ProductName, FirstDeliveryTime, and Every
553
554
  - SenderId: [ 1, 2, 3 ]
554
555
  ReceiverId: 10
555
556
  ProductName: MyProduct
556
557
  FirstDeliveryTime: 100
557
- DeliveryIntervalInSteps: 3600
558
+ Every: 1 hour
558
559
 
559
560
  # effectively 3 similar contracts (1 -> 11), (2 -> 12), (3 -> 13)
560
- # with otherwise identical ProductName, FirstDeliveryTime & DeliveryIntervalInSteps
561
+ # with otherwise identical ProductName, FirstDeliveryTime, and Every
561
562
  - SenderId: [ 1, 2, 3 ]
562
563
  ReceiverId: [ 11, 12, 13 ]
563
564
  ProductName: MyThirdProduct
564
565
  FirstDeliveryTime: 100
565
- DeliveryIntervalInSteps: 3600
566
+ Every: 1 hour
566
567
  ```
567
568
 
568
- Combined with YAML anchors complex contract chains can be easily reduced to a minimum of required configuration.
569
- The following example is equivalent to the previous one and allows a quick extension of contracts to a new couple of
570
- agents e.g. (4;14):
569
+ When combined with YAML anchors, the complexity of extensive contract chains can be reduced.
570
+ The following example is equivalent to the previous one, enabling contracts to be quickly extended to a new group of agents:
571
571
 
572
572
  ```yaml
573
573
  Groups:
574
- - &agentList1: [ 1,2,3 ]
575
- - &agentList2: [ 11,12,13 ]
574
+ - &agentList1: [ 1, 2, 3 ]
575
+ - &agentList2: [ 11, 12, 13 ]
576
576
 
577
577
  Contracts:
578
578
  - SenderId: 0
579
579
  ReceiverId: *agentList2
580
580
  ProductName: MyOtherProduct
581
581
  FirstDeliveryTime: 100
582
- DeliveryIntervalInSteps: 3600
582
+ Every: 1 hour
583
583
 
584
584
  - SenderId: *agentList1
585
585
  ReceiverId: 10
586
586
  ProductName: MyProduct
587
587
  FirstDeliveryTime: 100
588
- DeliveryIntervalInSteps: 3600
588
+ Every: 1 hour
589
589
 
590
590
  - SenderId: *agentList1
591
591
  ReceiverId: *agentList2
592
592
  ProductName: MyThirdProduct
593
593
  FirstDeliveryTime: 100
594
- DeliveryIntervalInSteps: 3600
594
+ Every: 1 hour
595
595
  ```
596
596
 
597
+ Lists can be nested in senders and receivers as follows:
598
+
599
+ ```
600
+ Groups:
601
+ - SenderId: 42
602
+ ReceiverId: [1, [2, 3], [4, [5]]]
603
+ ProductName: AnImportantProduct
604
+ FirstDeliveryTime: 101
605
+ Every: 30 minutes
606
+ ```
607
+
608
+ This feature should not be overused, as nested lists can become messy and difficult to read.
609
+ Special care should be taken when using it with an N-to-N mapping, as it will be difficult to check whether senders and receivers are matched correctly.
610
+
597
611
  #### StringSets
598
612
 
599
613
  This optional section defines values of type `string_set`.
@@ -475,7 +475,7 @@ Contracts:
475
475
  ReceiverId: 2
476
476
  ProductName: ProductOfAgent_1
477
477
  FirstDeliveryTime: -25
478
- DeliveryIntervalInSteps: 3600
478
+ Every: 3600
479
479
  Metadata:
480
480
  Some: "additional information can go here"
481
481
 
@@ -483,7 +483,7 @@ Contracts:
483
483
  ReceiverId: 1
484
484
  ProductName: ProductOfAgent_2
485
485
  FirstDeliveryTime: -22
486
- DeliveryIntervalInSteps: 3600
486
+ Every: 1 hour
487
487
  Attributes:
488
488
  ProductAppendix: value
489
489
  TimeOffset: 42
@@ -495,7 +495,8 @@ Contract Parameters:
495
495
  * `ReceiverId` unique ID of agent receiving the product
496
496
  * `ProductName` name of the product to be sent
497
497
  * `FirstDeliveryTime` first time of delivery in the format "seconds after the January 1st 2000, 00:00:00"
498
- * `DeliveryIntervalInSteps` delay time in between deliveries in seconds
498
+ * `Every` delay time in between deliveries; either an integer value in seconds, or a qualified time span in the format "<integer> <TimeUnit>(s)", where TimeUnit is one of "second", "minute", "hour", "day", "week", "month", "year" - with an options "s" at the end; mind that week, month, and year refer to fixed-length intervals of 168, 730, and 8760 hours.
499
+ * `DeliveryIntervalInSteps` deprecated; delay time in between deliveries in seconds (use instead of `Every`)
499
500
  * `Metadata` can be assigned to add further helpful information about a Contract
500
501
  * `Attributes` can be set to include additional information as `int`, `float`, `enum`, or `dict` data types
501
502
 
@@ -509,59 +510,72 @@ example:
509
510
  ```yaml
510
511
  Contracts:
511
512
  # effectively 3 similar contracts (0 -> 11), (0 -> 12), (0 -> 13)
512
- # with otherwise identical ProductName, FirstDeliveryTime & DeliveryIntervalInSteps
513
+ # with otherwise identical ProductName, FirstDeliveryTime, and Every
513
514
  - SenderId: 0
514
515
  ReceiverId: [ 11, 12, 13 ]
515
516
  ProductName: MyOtherProduct
516
517
  FirstDeliveryTime: 100
517
- DeliveryIntervalInSteps: 3600
518
+ Every: 1 hour
518
519
 
519
520
  # effectively 3 similar contracts (1 -> 10), (2 -> 10), (3 -> 10)
520
- # with otherwise identical ProductName, FirstDeliveryTime & DeliveryIntervalInSteps
521
+ # with otherwise identical ProductName, FirstDeliveryTime, and Every
521
522
  - SenderId: [ 1, 2, 3 ]
522
523
  ReceiverId: 10
523
524
  ProductName: MyProduct
524
525
  FirstDeliveryTime: 100
525
- DeliveryIntervalInSteps: 3600
526
+ Every: 1 hour
526
527
 
527
528
  # effectively 3 similar contracts (1 -> 11), (2 -> 12), (3 -> 13)
528
- # with otherwise identical ProductName, FirstDeliveryTime & DeliveryIntervalInSteps
529
+ # with otherwise identical ProductName, FirstDeliveryTime, and Every
529
530
  - SenderId: [ 1, 2, 3 ]
530
531
  ReceiverId: [ 11, 12, 13 ]
531
532
  ProductName: MyThirdProduct
532
533
  FirstDeliveryTime: 100
533
- DeliveryIntervalInSteps: 3600
534
+ Every: 1 hour
534
535
  ```
535
536
 
536
- Combined with YAML anchors complex contract chains can be easily reduced to a minimum of required configuration.
537
- The following example is equivalent to the previous one and allows a quick extension of contracts to a new couple of
538
- agents e.g. (4;14):
537
+ When combined with YAML anchors, the complexity of extensive contract chains can be reduced.
538
+ The following example is equivalent to the previous one, enabling contracts to be quickly extended to a new group of agents:
539
539
 
540
540
  ```yaml
541
541
  Groups:
542
- - &agentList1: [ 1,2,3 ]
543
- - &agentList2: [ 11,12,13 ]
542
+ - &agentList1: [ 1, 2, 3 ]
543
+ - &agentList2: [ 11, 12, 13 ]
544
544
 
545
545
  Contracts:
546
546
  - SenderId: 0
547
547
  ReceiverId: *agentList2
548
548
  ProductName: MyOtherProduct
549
549
  FirstDeliveryTime: 100
550
- DeliveryIntervalInSteps: 3600
550
+ Every: 1 hour
551
551
 
552
552
  - SenderId: *agentList1
553
553
  ReceiverId: 10
554
554
  ProductName: MyProduct
555
555
  FirstDeliveryTime: 100
556
- DeliveryIntervalInSteps: 3600
556
+ Every: 1 hour
557
557
 
558
558
  - SenderId: *agentList1
559
559
  ReceiverId: *agentList2
560
560
  ProductName: MyThirdProduct
561
561
  FirstDeliveryTime: 100
562
- DeliveryIntervalInSteps: 3600
562
+ Every: 1 hour
563
563
  ```
564
564
 
565
+ Lists can be nested in senders and receivers as follows:
566
+
567
+ ```
568
+ Groups:
569
+ - SenderId: 42
570
+ ReceiverId: [1, [2, 3], [4, [5]]]
571
+ ProductName: AnImportantProduct
572
+ FirstDeliveryTime: 101
573
+ Every: 30 minutes
574
+ ```
575
+
576
+ This feature should not be overused, as nested lists can become messy and difficult to read.
577
+ Special care should be taken when using it with an N-to-N mapping, as it will be difficult to check whether senders and receivers are matched correctly.
578
+
565
579
  #### StringSets
566
580
 
567
581
  This optional section defines values of type `string_set`.
@@ -7,7 +7,7 @@ build-backend = "poetry.core.masonry.api"
7
7
 
8
8
  [project]
9
9
  name = "fameio"
10
- version = "3.3.0"
10
+ version = "3.4.0"
11
11
  description = "Tools for input preparation and output digestion of FAME models"
12
12
  license = "Apache-2.0"
13
13
  readme = "README.md"
@@ -10,7 +10,7 @@ from fameio.input.metadata import Metadata
10
10
  from fameio.input.scenario.attribute import Attribute
11
11
  from fameio.logs import log, log_error
12
12
  from fameio.time import FameTime, ConversionError
13
- from fameio.tools import ensure_is_list, keys_to_lower
13
+ from fameio.tools import keys_to_lower
14
14
 
15
15
 
16
16
  class Contract(Metadata):
@@ -24,6 +24,7 @@ class Contract(Metadata):
24
24
  KEY_PRODUCT: Final[str] = "ProductName".lower()
25
25
  KEY_FIRST_DELIVERY: Final[str] = "FirstDeliveryTime".lower()
26
26
  KEY_INTERVAL: Final[str] = "DeliveryIntervalInSteps".lower()
27
+ KEY_EVERY: Final[str] = "Every".lower()
27
28
  KEY_EXPIRE: Final[str] = "ExpirationTime".lower()
28
29
  KEY_ATTRIBUTES: Final[str] = "Attributes".lower()
29
30
 
@@ -33,7 +34,8 @@ class Contract(Metadata):
33
34
  "or N-to-N sender-to-receiver numbers. Found M-to-N pairing in Contract with "
34
35
  "Senders: {} and Receivers: {}."
35
36
  )
36
- _ERR_INTERVAL_NOT_POSITIVE = "Contract delivery interval must be a positive integer but was: {}"
37
+ _ERR_XOR_KEYS = "Contract expects exactly one of the keys '{}' or '{}'. Found either both or none."
38
+ _ERR_INTERVAL_INVALID = "Contract delivery interval must be a positive integer but was: {}"
37
39
  _ERR_SENDER_IS_RECEIVER = "Contract sender and receiver have the same id: {}"
38
40
  _ERR_DOUBLE_ATTRIBUTE = "Cannot add attribute '{}' to contract because it already exists."
39
41
  _ERR_TIME_CONVERSION = "Contract item '{}' is an ill-formatted time: '{}'"
@@ -73,7 +75,7 @@ class Contract(Metadata):
73
75
  if sender_id == receiver_id:
74
76
  log().warning(self._ERR_SENDER_IS_RECEIVER.format(sender_id))
75
77
  if delivery_interval <= 0:
76
- raise log_error(self.ContractError(self._ERR_INTERVAL_NOT_POSITIVE.format(delivery_interval)))
78
+ raise log_error(self.ContractError(self._ERR_INTERVAL_INVALID.format(delivery_interval)))
77
79
  self._sender_id = sender_id
78
80
  self._receiver_id = receiver_id
79
81
  self._product_name = product_name
@@ -164,7 +166,7 @@ class Contract(Metadata):
164
166
  product_name = Contract._get_or_raise(definitions, Contract.KEY_PRODUCT, Contract._ERR_MISSING_KEY)
165
167
 
166
168
  first_delivery_time = Contract._get_time(definitions, Contract.KEY_FIRST_DELIVERY)
167
- delivery_interval = Contract._get_or_raise(definitions, Contract.KEY_INTERVAL, Contract._ERR_MISSING_KEY)
169
+ delivery_interval = Contract._get_interval(definitions)
168
170
  expiration_time = Contract._get_time(definitions, Contract.KEY_EXPIRE, mandatory=False)
169
171
 
170
172
  contract = cls(sender_id, receiver_id, product_name, delivery_interval, first_delivery_time, expiration_time)
@@ -224,6 +226,43 @@ class Contract(Metadata):
224
226
  raise log_error(Contract.ContractError(Contract._ERR_MISSING_KEY.format(key)))
225
227
  return None
226
228
 
229
+ @staticmethod
230
+ def _get_interval(definitions: dict) -> int:
231
+ """Extract delivery interval from Contract definition, or raise an error if not present or ill formatted.
232
+
233
+ Args:
234
+ definitions: to extract the delivery interval from
235
+
236
+ Returns:
237
+ the delivery interval in fame time steps
238
+
239
+ Raises:
240
+ ContractError: if delivery interval is not defined or invalid, logged with level "ERROR"
241
+ """
242
+ has_interval = Contract.KEY_INTERVAL in definitions
243
+ has_every = Contract.KEY_EVERY in definitions
244
+
245
+ if has_interval and not has_every:
246
+ value = definitions[Contract.KEY_INTERVAL]
247
+ if isinstance(value, int):
248
+ return value
249
+ raise log_error(Contract.ContractError(Contract._ERR_INTERVAL_INVALID.format(value)))
250
+ if has_every and not has_interval:
251
+ value = definitions[Contract.KEY_EVERY]
252
+ if isinstance(value, int):
253
+ return value
254
+ if isinstance(value, str):
255
+ try:
256
+ return FameTime.convert_text_to_time_span(value)
257
+ except ConversionError as e:
258
+ raise log_error(
259
+ Contract.ContractError(Contract._ERR_TIME_CONVERSION.format(Contract.KEY_EVERY, value))
260
+ ) from e
261
+ raise log_error(Contract.ContractError(Contract._ERR_TIME_CONVERSION.format(Contract.KEY_EVERY, value)))
262
+ raise log_error(
263
+ Contract.ContractError(Contract._ERR_XOR_KEYS.format(Contract.KEY_INTERVAL, Contract.KEY_EVERY))
264
+ )
265
+
227
266
  def _init_attributes_from_dict(self, attributes: dict[str, Any]) -> None:
228
267
  """Resets Contract `attributes` from dict.
229
268
 
@@ -280,15 +319,14 @@ class Contract(Metadata):
280
319
  Contract.KEY_EXPIRE,
281
320
  Contract.KEY_METADATA,
282
321
  Contract.KEY_ATTRIBUTES,
322
+ Contract.KEY_EVERY,
283
323
  ]:
284
324
  if key in multi_definition:
285
325
  base_data[key] = multi_definition[key]
286
- senders = ensure_is_list(
287
- Contract._get_or_raise(multi_definition, Contract.KEY_SENDER, Contract._ERR_MISSING_KEY)
288
- )
289
- receivers = ensure_is_list(
290
- Contract._get_or_raise(multi_definition, Contract.KEY_RECEIVER, Contract._ERR_MISSING_KEY)
291
- )
326
+ sender_value = Contract._get_or_raise(multi_definition, Contract.KEY_SENDER, Contract._ERR_MISSING_KEY)
327
+ senders = Contract._unpack_list(sender_value)
328
+ receiver_value = Contract._get_or_raise(multi_definition, Contract.KEY_RECEIVER, Contract._ERR_MISSING_KEY)
329
+ receivers = Contract._unpack_list(receiver_value)
292
330
  if len(senders) > 1 and len(receivers) == 1:
293
331
  for index, sender in enumerate(senders):
294
332
  contracts.append(Contract._copy_contract(sender, receivers[0], base_data))
@@ -302,6 +340,13 @@ class Contract(Metadata):
302
340
  raise log_error(Contract.ContractError(Contract._ERR_MULTI_CONTRACT_CORRUPT.format(senders, receivers)))
303
341
  return contracts
304
342
 
343
+ @staticmethod
344
+ def _unpack_list(obj: Any | list) -> list[Any]:
345
+ """Returns the given value as a flat list - unpacks potential nested list(s)"""
346
+ if isinstance(obj, list):
347
+ return [item for element in obj for item in Contract._unpack_list(element)]
348
+ return [obj]
349
+
305
350
  @staticmethod
306
351
  def _copy_contract(sender: int, receiver: int, base_data: dict) -> dict:
307
352
  """Returns a new contract definition dictionary, with given `sender` and `receiver` and copied `base_data`."""
@@ -17,6 +17,7 @@ START_IN_REAL_TIME = "2000-01-01_00:00:00"
17
17
  DATE_FORMAT = "%Y-%m-%d_%H:%M:%S"
18
18
  DATE_REGEX = re.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}:[0-9]{2}:[0-9]{2}")
19
19
  FAME_FIRST_DATETIME = dt.datetime.strptime(START_IN_REAL_TIME, DATE_FORMAT)
20
+ TIME_SPAN_REGEX = re.compile(r"^[0-9]+\s*[A-z]+$")
20
21
 
21
22
 
22
23
  class ConversionError(InputError, OutputError):
@@ -71,6 +72,8 @@ class FameTime:
71
72
  _INVALID_TOO_LARGE = "Cannot convert time stamp string '{}' - last day of leap year is Dec 30th!"
72
73
  _NO_TIMESTAMP = "Time value expected, but '{}' is neither a time stamp string nor an integer."
73
74
  _INVALID_DATE_FORMAT = "Received invalid date format '{}'."
75
+ _INVALID_SPAN_FORMAT = "Time span must be provided in the format '<positive integer> <TimeUnit>' but was: '{}'"
76
+ _INVALID_TIME_UNIT = f"Time span unit '{{}}' unknown, must be one of: {[u.name for u in TimeUnit]}"
74
77
 
75
78
  @staticmethod
76
79
  def convert_datetime_to_fame_time_step(datetime_string: str) -> int:
@@ -160,6 +163,32 @@ class FameTime:
160
163
  return steps * value
161
164
  raise log_error(ConversionError(FameTime._TIME_UNIT_UNKNOWN.format(unit)))
162
165
 
166
+ @staticmethod
167
+ def convert_text_to_time_span(string: str) -> int:
168
+ """Converts given string in form of "<positive integer> <TimeUnit>" to a time span in FAME time steps.
169
+
170
+ Args:
171
+ string: to convert to a time span from
172
+
173
+ Returns:
174
+ FAME time steps equivalent of `value x unit`
175
+
176
+ Raises:
177
+ ConversionError: if an unknown time unit or an unsupported format is used, logged with level "ERROR"
178
+ """
179
+ string = string.strip()
180
+ if TIME_SPAN_REGEX.fullmatch(string) is None:
181
+ raise log_error(ConversionError(FameTime._INVALID_SPAN_FORMAT.format(string)))
182
+ multiple, unit_name = string.split()
183
+ unit_name = unit_name.upper()
184
+ if not unit_name.endswith("S"):
185
+ unit_name += "S"
186
+ try:
187
+ unit = TimeUnit[unit_name]
188
+ except KeyError as e:
189
+ raise log_error(ConversionError(FameTime._INVALID_TIME_UNIT.format(unit_name))) from e
190
+ return FameTime.convert_time_span_to_fame_time_steps(int(multiple), unit)
191
+
163
192
  @staticmethod
164
193
  def is_datetime(string: Any) -> bool:
165
194
  """Returns `True` if given `string` matches Datetime string format and can be converted to FAME time step."""
@@ -208,3 +237,15 @@ class FameTime:
208
237
  return int(value)
209
238
  except ValueError as e:
210
239
  raise log_error(ConversionError(FameTime._NO_TIMESTAMP.format(value))) from e
240
+
241
+ @staticmethod
242
+ def get_first_fame_time_step_of_year(year: int) -> int:
243
+ """Returns FAME time in integer format of first time step of given year.
244
+
245
+ Args:
246
+ year: to get the first time step for
247
+
248
+ Returns:
249
+ first time step in the requested year in integer format
250
+ """
251
+ return (year - FAME_FIRST_DATETIME.year) * Constants.STEPS_PER_YEAR
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes