fameio 3.3.0__py3-none-any.whl → 3.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.
@@ -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`."""
fameio/time.py CHANGED
@@ -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
@@ -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`.
@@ -14,7 +14,7 @@ fameio/input/resolver.py,sha256=NakBjnCCWRMz-8gTC_Ggx-2tXq-u6OPjfBOua0Rd2nA,1902
14
14
  fameio/input/scenario/__init__.py,sha256=Pb8O9rVOTwEo48WIgiq1kBnpovpc4D_syC6EjTFGHew,404
15
15
  fameio/input/scenario/agent.py,sha256=n0H8nHwQFfAeTwdJceJpi9VV1muYjJu_PjmzC_vrP84,5526
16
16
  fameio/input/scenario/attribute.py,sha256=pp9cquxfBUKNFwV3bTDBkDXv1k8ThBiAL6Eh-ag4kQk,11386
17
- fameio/input/scenario/contract.py,sha256=QPjsChndKMXphIpB_f0IeAiksZG7xz8U-xeLkvYRJUA,13211
17
+ fameio/input/scenario/contract.py,sha256=fv4XRMMF8X9Q1LMNJsWcPfQx-EY40gDBWsEMJ7rsaBQ,15324
18
18
  fameio/input/scenario/exception.py,sha256=o64hd7FQrkTF6Ze075Cbt4TM3OkcyJFVSi4qexLuMoU,1939
19
19
  fameio/input/scenario/fameiofactory.py,sha256=HgLHVQGKsTPEFy8K1ILB7F_lJtHoMhu89inOgDWYP5k,2800
20
20
  fameio/input/scenario/generalproperties.py,sha256=C3ND-PLb1FrCRheIBIyxXHKsZVMW8GZVHUidyInqrOw,3749
@@ -48,13 +48,13 @@ fameio/scripts/make_config.py.license,sha256=EXKiZn-aoR7nO3auGMNGk9upHbobPLHAIBY
48
48
  fameio/scripts/reformat.py,sha256=jYJsl0UkXtZyn2GyA-QVAARilkHa_ZBWa5CGNIGNDuo,2850
49
49
  fameio/scripts/reformat.py.license,sha256=EXKiZn-aoR7nO3auGMNGk9upHbobPLHAIBYUO0S6LUg,107
50
50
  fameio/series.py,sha256=ipjDsDmgVlVzaYvXwXcWM8fpCy5w8hTNWTi4hPmvOuM,13654
51
- fameio/time.py,sha256=rJbcX304bjmZqmr7DozwZAvvNf4FCpxUYjUppl0BFb0,8431
51
+ fameio/time.py,sha256=S8TagfGA7B44JPkj8YfC3ftkxODNTC6ZXELNQyHC1VA,10201
52
52
  fameio/tools.py,sha256=metmgKuZ0lubmTIPY3w_ertDxSLQtHIa6OlpcapyIk4,2478
53
- fameio-3.3.0.dist-info/entry_points.txt,sha256=IUbTceB_CLFOHulubEf9jgiCFsV2TchlzCssmjbiOKI,176
54
- fameio-3.3.0.dist-info/LICENSE.txt,sha256=eGHBZnhr9CWjE95SWjRfmhtK1lvVn5X4Fpf3KrrAZDg,10391
55
- fameio-3.3.0.dist-info/LICENSES/Apache-2.0.txt,sha256=eGHBZnhr9CWjE95SWjRfmhtK1lvVn5X4Fpf3KrrAZDg,10391
56
- fameio-3.3.0.dist-info/LICENSES/CC-BY-4.0.txt,sha256=y9WvMYKGt0ZW8UXf9QkZB8wj1tjJrQngKR7CSXeSukE,19051
57
- fameio-3.3.0.dist-info/LICENSES/CC0-1.0.txt,sha256=9Ofzc7m5lpUDN-jUGkopOcLZC3cl6brz1QhKInF60yg,7169
58
- fameio-3.3.0.dist-info/METADATA,sha256=10ZT26KgUXe2-ko_ZFfmqS2Z9eLTzim_9PST-aQwOHQ,41721
59
- fameio-3.3.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
60
- fameio-3.3.0.dist-info/RECORD,,
53
+ fameio-3.4.0.dist-info/entry_points.txt,sha256=IUbTceB_CLFOHulubEf9jgiCFsV2TchlzCssmjbiOKI,176
54
+ fameio-3.4.0.dist-info/LICENSE.txt,sha256=eGHBZnhr9CWjE95SWjRfmhtK1lvVn5X4Fpf3KrrAZDg,10391
55
+ fameio-3.4.0.dist-info/LICENSES/Apache-2.0.txt,sha256=eGHBZnhr9CWjE95SWjRfmhtK1lvVn5X4Fpf3KrrAZDg,10391
56
+ fameio-3.4.0.dist-info/LICENSES/CC-BY-4.0.txt,sha256=y9WvMYKGt0ZW8UXf9QkZB8wj1tjJrQngKR7CSXeSukE,19051
57
+ fameio-3.4.0.dist-info/LICENSES/CC0-1.0.txt,sha256=9Ofzc7m5lpUDN-jUGkopOcLZC3cl6brz1QhKInF60yg,7169
58
+ fameio-3.4.0.dist-info/METADATA,sha256=qRURN8lvabRE9CmzKRuwsoi_1rKyv71TGkkdLJUAydE,42374
59
+ fameio-3.4.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
60
+ fameio-3.4.0.dist-info/RECORD,,
File without changes