fameio 3.1.1__py3-none-any.whl → 3.2.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 (52) hide show
  1. fameio/cli/convert_results.py +6 -4
  2. fameio/cli/make_config.py +6 -4
  3. fameio/cli/parser.py +41 -29
  4. fameio/input/__init__.py +1 -1
  5. fameio/input/loader/__init__.py +9 -7
  6. fameio/input/loader/controller.py +59 -8
  7. fameio/input/loader/loader.py +14 -7
  8. fameio/input/metadata.py +35 -13
  9. fameio/input/resolver.py +5 -4
  10. fameio/input/scenario/agent.py +50 -16
  11. fameio/input/scenario/attribute.py +14 -15
  12. fameio/input/scenario/contract.py +152 -43
  13. fameio/input/scenario/exception.py +44 -18
  14. fameio/input/scenario/fameiofactory.py +63 -7
  15. fameio/input/scenario/generalproperties.py +17 -6
  16. fameio/input/scenario/scenario.py +111 -28
  17. fameio/input/scenario/stringset.py +27 -8
  18. fameio/input/schema/agenttype.py +21 -2
  19. fameio/input/schema/attribute.py +91 -22
  20. fameio/input/schema/java_packages.py +8 -5
  21. fameio/input/schema/schema.py +35 -9
  22. fameio/input/validator.py +22 -15
  23. fameio/input/writer.py +136 -36
  24. fameio/logs.py +3 -31
  25. fameio/output/__init__.py +5 -1
  26. fameio/output/agent_type.py +86 -23
  27. fameio/output/conversion.py +47 -29
  28. fameio/output/csv_writer.py +88 -18
  29. fameio/output/data_transformer.py +7 -14
  30. fameio/output/input_dao.py +62 -21
  31. fameio/output/output_dao.py +26 -4
  32. fameio/output/reader.py +58 -13
  33. fameio/output/yaml_writer.py +15 -6
  34. fameio/scripts/__init__.py +9 -2
  35. fameio/scripts/convert_results.py +123 -50
  36. fameio/scripts/convert_results.py.license +1 -1
  37. fameio/scripts/exception.py +7 -0
  38. fameio/scripts/make_config.py +34 -12
  39. fameio/scripts/make_config.py.license +1 -1
  40. fameio/series.py +117 -33
  41. fameio/time.py +74 -17
  42. fameio/tools.py +7 -5
  43. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/METADATA +19 -13
  44. fameio-3.2.0.dist-info/RECORD +56 -0
  45. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/WHEEL +1 -1
  46. CHANGELOG.md +0 -288
  47. fameio-3.1.1.dist-info/RECORD +0 -56
  48. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSE.txt +0 -0
  49. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  50. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
  51. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
  52. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/entry_points.txt +0 -0
@@ -1,34 +1,56 @@
1
1
  #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
2
4
  import sys
3
5
  from pathlib import Path
6
+ from typing import Any
4
7
 
8
+ from fameio.cli import update_default_config
5
9
  from fameio.cli.make_config import handle_args, CLI_DEFAULTS as DEFAULT_CONFIG
6
10
  from fameio.cli.options import Options
7
- from fameio.cli import update_default_config
11
+ from fameio.input import InputError
8
12
  from fameio.input.loader import load_yaml, validate_yaml_file_suffix
9
- from fameio.logs import fameio_logger, log
10
13
  from fameio.input.scenario import Scenario
11
14
  from fameio.input.validator import SchemaValidator
12
15
  from fameio.input.writer import ProtoWriter
16
+ from fameio.logs import fameio_logger, log, log_critical
17
+ from fameio.scripts.exception import ScriptError
18
+
19
+ _ERR_FAIL: str = "Creation of run configuration file failed."
20
+
21
+
22
+ def run(config: dict[Options, Any] | None = None) -> None:
23
+ """
24
+ Executes the main workflow of building a FAME configuration file
13
25
 
26
+ Args:
27
+ config: configuration options
14
28
 
15
- def run(config: dict = None) -> None:
16
- """Executes the main workflow for the building of a FAME configuration file"""
29
+ Raises:
30
+ ScriptError: if any kind of expected error occurred, logged with level "CRITICAL"
31
+ """
17
32
  config = update_default_config(config, DEFAULT_CONFIG)
18
33
  fameio_logger(log_level_name=config[Options.LOG_LEVEL], file_name=config[Options.LOG_FILE])
19
34
 
20
- file = config[Options.FILE]
21
- validate_yaml_file_suffix(Path(file))
22
- scenario = Scenario.from_dict(load_yaml(Path(file), encoding=config[Options.INPUT_ENCODING]))
23
- SchemaValidator.check_agents_have_contracts(scenario)
35
+ try:
36
+ file = config[Options.FILE]
37
+ validate_yaml_file_suffix(Path(file))
38
+ scenario_definition = load_yaml(Path(file), encoding=config[Options.INPUT_ENCODING])
39
+ scenario = Scenario.from_dict(scenario_definition)
40
+ SchemaValidator.check_agents_have_contracts(scenario)
24
41
 
25
- timeseries_manager = SchemaValidator.validate_scenario_and_timeseries(scenario)
26
- writer = ProtoWriter(config[Options.OUTPUT], timeseries_manager)
27
- writer.write_validated_scenario(scenario)
42
+ timeseries_manager = SchemaValidator.validate_scenario_and_timeseries(scenario)
43
+ writer = ProtoWriter(config[Options.OUTPUT], timeseries_manager)
44
+ writer.write_validated_scenario(scenario)
45
+ except InputError as ex:
46
+ raise log_critical(ScriptError(_ERR_FAIL)) from ex
28
47
 
29
48
  log().info("Configuration completed.")
30
49
 
31
50
 
32
51
  if __name__ == "__main__":
33
52
  run_config = handle_args(sys.argv[1:])
34
- run(run_config)
53
+ try:
54
+ run(run_config)
55
+ except ScriptError as e:
56
+ raise SystemExit(1) from e
@@ -1,3 +1,3 @@
1
- SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
1
+ SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
2
2
 
3
3
  SPDX-License-Identifier: Apache-2.0
fameio/series.py CHANGED
@@ -1,26 +1,30 @@
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
  import os
6
8
  from enum import Enum, auto
7
9
  from pathlib import Path
8
- from typing import Union, Any
10
+ from typing import Any
9
11
 
10
12
  import pandas as pd
11
13
  from fameprotobuf.input_file_pb2 import InputData
12
14
  from google.protobuf.internal.wire_format import INT64_MIN, INT64_MAX
13
15
 
16
+ from fameio.input import InputError
14
17
  from fameio.input.resolver import PathResolver
15
18
  from fameio.logs import log, log_error
19
+ from fameio.output import OutputError
16
20
  from fameio.time import ConversionError, FameTime
17
21
  from fameio.tools import clean_up_file_name
18
22
 
19
-
20
23
  CSV_FILE_SUFFIX = ".csv"
24
+ FILE_LENGTH_WARN_LIMIT = int(50e3)
21
25
 
22
26
 
23
- class TimeSeriesError(Exception):
27
+ class TimeSeriesError(InputError, OutputError):
24
28
  """Indicates that an error occurred during management of time series"""
25
29
 
26
30
 
@@ -41,19 +45,25 @@ class TimeSeriesManager:
41
45
  _ERR_FILE_NOT_FOUND = "Cannot find Timeseries file '{}'."
42
46
  _ERR_NUMERIC_STRING = " Remove quotes to use a constant numeric value instead of a timeseries file."
43
47
  _ERR_CORRUPT_TIME_SERIES_KEY = "TimeSeries file '{}' corrupt: At least one entry in first column isn't a timestamp."
44
- _ERR_CORRUPT_TIME_SERIES_VALUE = "TimeSeries file '{}' corrupt: At least one entry in value column isn't numeric."
48
+ _ERR_CORRUPT_TIME_SERIES_VALUE = "TimeSeries file '{}' corrupt: At least one entry in second column isn't numeric."
45
49
  _ERR_NON_NUMERIC = "Values in TimeSeries must be numeric but was: '{}'"
46
50
  _ERR_NAN_VALUE = "Values in TimeSeries must not be missing or NaN."
47
51
  _ERR_UNREGISTERED_SERIES = "No timeseries registered with identifier '{}' - was the Scenario validated?"
52
+ _ERR_UNREGISTERED_SERIES_RE = "No timeseries registered with identifier '{}' - were the timeseries reconstructed?"
48
53
  _WARN_NO_DATA = "No timeseries stored in timeseries manager. Double check if you expected timeseries."
49
54
  _WARN_DATA_IGNORED = "Timeseries contains additional columns with data which will be ignored."
55
+ _WARN_LARGE_CONVERSION = (
56
+ "Timeseries file '{}' is large and needs conversion of time stamps. If performance "
57
+ "issues occur and the file is reused, convert the time stamp column once with "
58
+ "`fameio.time.FameTime.convert_datetime_to_fame_time_step(datetime_string)`."
59
+ )
50
60
 
51
61
  def __init__(self, path_resolver: PathResolver = PathResolver()) -> None:
52
62
  self._path_resolver = path_resolver
53
63
  self._id_count = -1
54
- self._series_by_id: dict[Union[str, int, float], dict[Entry, Any]] = {}
64
+ self._series_by_id: dict[str | int | float, dict[Entry, Any]] = {}
55
65
 
56
- def register_and_validate(self, identifier: Union[str, int, float]) -> None:
66
+ def register_and_validate(self, identifier: str | int | float) -> None:
57
67
  """
58
68
  Registers given timeseries `identifier` and validates associated timeseries
59
69
 
@@ -61,29 +71,54 @@ class TimeSeriesManager:
61
71
  identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
62
72
 
63
73
  Raises:
64
- TimeSeriesException: if file was not found, ill-formatted, or value was invalid
74
+ TimeSeriesError: if the file could not be found or contains improper data, or if identifier is NaN,
75
+ logged with level "ERROR"
65
76
  """
66
77
  if not self._time_series_is_registered(identifier):
67
78
  self._register_time_series(identifier)
68
79
 
69
- def _time_series_is_registered(self, identifier: Union[str, int, float]) -> bool:
80
+ def _time_series_is_registered(self, identifier: str | int | float) -> bool:
70
81
  """Returns True if the value was already registered"""
71
82
  return identifier in self._series_by_id
72
83
 
73
- def _register_time_series(self, identifier: Union[str, int, float]) -> None:
74
- """Assigns an id to the given `identifier` and loads the time series into a dataframe"""
84
+ def _register_time_series(self, identifier: str | int | float) -> None:
85
+ """
86
+ Assigns an id to the given `identifier` and loads the time series into a dataframe
87
+
88
+ Args:
89
+ identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
90
+
91
+ Raises:
92
+ TimeSeriesError: if the file could not be found or contains improper data, or if identifier is NaN,
93
+ logged with level "ERROR"
94
+ """
75
95
  self._id_count += 1
76
96
  name, series = self._get_name_and_dataframe(identifier)
77
97
  self._series_by_id[identifier] = {Entry.ID: self._id_count, Entry.NAME: name, Entry.DATA: series}
78
98
 
79
- def _get_name_and_dataframe(self, identifier: Union[str, int, float]) -> tuple[str, pd.DataFrame]:
80
- """Returns name and DataFrame containing the series obtained from the given `identifier`"""
99
+ def _get_name_and_dataframe(self, identifier: str | int | float) -> tuple[str, pd.DataFrame]:
100
+ """
101
+ Returns name and DataFrame containing the series obtained from the given `identifier`
102
+
103
+ Args:
104
+ identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
105
+
106
+ Returns:
107
+ tuple of name & dataframe
108
+
109
+ Raises:
110
+ TimeSeriesError: if the file could not be found or contains improper data, or if identifier is NaN,
111
+ logged with level "ERROR"
112
+ """
81
113
  if isinstance(identifier, str):
82
114
  series_path = self._path_resolver.resolve_series_file_path(Path(identifier).as_posix())
83
115
  if series_path and os.path.exists(series_path):
84
- data = pd.read_csv(series_path, sep=";", header=None, comment="#")
85
116
  try:
86
- return identifier, self._check_and_convert_series(data)
117
+ data = pd.read_csv(series_path, sep=";", header=None, comment="#")
118
+ except OSError as e:
119
+ raise log_error(TimeSeriesError(e)) from e
120
+ try:
121
+ return identifier, self._check_and_convert_series(data, identifier)
87
122
  except TypeError as e:
88
123
  raise log_error(TimeSeriesError(self._ERR_CORRUPT_TIME_SERIES_VALUE.format(identifier), e)) from e
89
124
  except ConversionError as e:
@@ -96,26 +131,52 @@ class TimeSeriesManager:
96
131
  else:
97
132
  return self._create_timeseries_from_value(identifier)
98
133
 
99
- def _check_and_convert_series(self, data: pd.DataFrame) -> pd.DataFrame:
100
- """Ensures validity of time series and convert to required format for writing to disk"""
101
- additional_columns = data.loc[:, 2:]
102
- is_empty = additional_columns.dropna(how="all").empty
103
- if not is_empty:
134
+ def _check_and_convert_series(self, data: pd.DataFrame, identifier: str) -> pd.DataFrame:
135
+ """
136
+ Ensures validity of time series and convert to required format for writing to disk
137
+
138
+ Args:
139
+ data: dataframe to be converted to expected format
140
+
141
+ Returns:
142
+ 2-column dataframe, first column: integers, second column: floats (no NaN)
143
+
144
+ Raises:
145
+ ConversionError: if first data column could not be converted to integer, logged with level "ERROR"
146
+ TypeError: if second data column in given data could not be converted to float or contained NaN,
147
+ logged with level "ERROR"
148
+ """
149
+ data, additional_columns = data.loc[:, :2], data.loc[:, 2:]
150
+ if not additional_columns.dropna(how="all").empty:
104
151
  log().warning(self._WARN_DATA_IGNORED)
105
152
  if data.dtypes[0] != "int64":
153
+ if len(data[0]) > FILE_LENGTH_WARN_LIMIT:
154
+ log().warning(self._WARN_LARGE_CONVERSION.format(identifier))
106
155
  data[0] = [FameTime.convert_string_if_is_datetime(time) for time in data[0]]
107
- data[1] = [TimeSeriesManager._assert_valid(value) for value in data[1]]
156
+ if data.dtypes[1] != "float64":
157
+ data[1] = [TimeSeriesManager._assert_float(value) for value in data[1]]
158
+ if data[1].isna().any():
159
+ raise log_error(TypeError(TimeSeriesManager._ERR_NAN_VALUE))
108
160
  return data
109
161
 
110
162
  @staticmethod
111
- def _assert_valid(value: Any) -> float:
112
- """Returns the given `value` if it is a numeric value other than NaN"""
163
+ def _assert_float(value: Any) -> float:
164
+ """
165
+ Converts any given value to a float or raise an Exception
166
+
167
+ Args:
168
+ value: to be converted to float
169
+
170
+ Returns:
171
+ float representation of value
172
+
173
+ Raises:
174
+ TypeError: if given value cannot be converted to float, logged with level "ERROR"
175
+ """
113
176
  try:
114
177
  value = float(value)
115
178
  except ValueError as e:
116
179
  raise log_error(TypeError(TimeSeriesManager._ERR_NON_NUMERIC.format(value))) from e
117
- if math.isnan(value):
118
- raise log_error(TypeError(TimeSeriesManager._ERR_NAN_VALUE))
119
180
  return value
120
181
 
121
182
  @staticmethod
@@ -128,14 +189,25 @@ class TimeSeriesManager:
128
189
  return False
129
190
 
130
191
  @staticmethod
131
- def _create_timeseries_from_value(value: Union[int, float]) -> tuple[str, pd.DataFrame]:
132
- """Returns name and dataframe for a new static timeseries created from the given `value`"""
192
+ def _create_timeseries_from_value(value: int | float) -> tuple[str, pd.DataFrame]:
193
+ """
194
+ Returns name and dataframe for a new static timeseries created from the given `value`
195
+
196
+ Args:
197
+ value: the static value of the timeseries to be created
198
+
199
+ Returns:
200
+ tuple of name & dataframe
201
+
202
+ Raises:
203
+ TimeSeriesError: if given value is NaN, logged with level "ERROR"
204
+ """
133
205
  if math.isnan(value):
134
206
  raise log_error(TimeSeriesError(TimeSeriesManager._ERR_NAN_VALUE))
135
207
  data = pd.DataFrame({0: [INT64_MIN, INT64_MAX], 1: [value, value]})
136
208
  return TimeSeriesManager._CONSTANT_IDENTIFIER.format(value), data
137
209
 
138
- def get_series_id_by_identifier(self, identifier: Union[str, int, float]) -> int:
210
+ def get_series_id_by_identifier(self, identifier: str | int | float) -> int:
139
211
  """
140
212
  Returns id for a previously stored time series by given `identifier`
141
213
 
@@ -146,11 +218,11 @@ class TimeSeriesManager:
146
218
  unique ID for the given identifier
147
219
 
148
220
  Raises:
149
- TimeSeriesException: if identifier was not yet registered
221
+ TimeSeriesError: if identifier was not yet registered, logged with level "ERROR"
150
222
  """
151
223
  if not self._time_series_is_registered(identifier):
152
224
  raise log_error(TimeSeriesError(self._ERR_UNREGISTERED_SERIES.format(identifier)))
153
- return self._series_by_id.get(identifier)[Entry.ID]
225
+ return self._series_by_id.get(identifier)[Entry.ID] # type: ignore[index]
154
226
 
155
227
  def get_all_series(self) -> list[tuple[int, str, pd.DataFrame]]:
156
228
  """Returns iterator over id, name and dataframe of all stored series"""
@@ -175,7 +247,8 @@ class TimeSeriesManager:
175
247
  )
176
248
  self._series_by_id[one_series.series_id] = reconstructed
177
249
 
178
- def _get_cleaned_file_name(self, timeseries_name: str):
250
+ def _get_cleaned_file_name(self, timeseries_name: str) -> str:
251
+ """Ensure given file name has CSV file ending"""
179
252
  if Path(timeseries_name).suffix.lower() == CSV_FILE_SUFFIX:
180
253
  filename = Path(timeseries_name).name
181
254
  else:
@@ -183,8 +256,19 @@ class TimeSeriesManager:
183
256
  return str(Path(self._TIMESERIES_RECONSTRUCTION_PATH, filename))
184
257
 
185
258
  def get_reconstructed_series_by_id(self, series_id: int) -> str:
186
- """Return name or path for given `series_id` if series these are identified by their number.
187
- Use this only if series were added via `reconstruct_time_series`"""
259
+ """
260
+ Return name or path for given `series_id` if series these are identified by their number.
261
+ Use this only if series were added via `reconstruct_time_series`
262
+
263
+ Args:
264
+ series_id: number of series
265
+
266
+ Returns:
267
+ name or path of time series
268
+
269
+ Raises:
270
+ TimeSeriesError: if series was not registered during `reconstruct_time_series`, logged with level "ERROR"
271
+ """
188
272
  if series_id < 0 or series_id > self._id_count:
189
- raise log_error(TimeSeriesError(self._ERR_UNREGISTERED_SERIES.format(series_id)))
273
+ raise log_error(TimeSeriesError(self._ERR_UNREGISTERED_SERIES_RE.format(series_id)))
190
274
  return self._series_by_id[series_id][Entry.NAME]
fameio/time.py CHANGED
@@ -1,13 +1,17 @@
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 datetime as dt
5
7
  import math
6
8
  import re
7
9
  from enum import Enum, auto
8
- from typing import Union
10
+ from typing import Any
9
11
 
12
+ from fameio.input import InputError
10
13
  from fameio.logs import log_error
14
+ from fameio.output import OutputError
11
15
 
12
16
  START_IN_REAL_TIME = "2000-01-01_00:00:00"
13
17
  DATE_FORMAT = "%Y-%m-%d_%H:%M:%S"
@@ -15,8 +19,8 @@ DATE_REGEX = re.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}:[0-9]{2}:[0-9]{2}")
15
19
  FAME_FIRST_DATETIME = dt.datetime.strptime(START_IN_REAL_TIME, DATE_FORMAT)
16
20
 
17
21
 
18
- class ConversionError(Exception):
19
- """Indicates that something went wrong during time stamp conversion"""
22
+ class ConversionError(InputError, OutputError):
23
+ """Indicates that time stamp conversion failed"""
20
24
 
21
25
 
22
26
  class TimeUnit(Enum):
@@ -45,9 +49,9 @@ class Constants:
45
49
  STEPS_PER_DAY = STEPS_PER_HOUR * HOURS_PER_DAY
46
50
  STEPS_PER_YEAR = STEPS_PER_DAY * DAYS_PER_YEAR
47
51
  STEPS_PER_WEEK = STEPS_PER_DAY * 7
48
- STEPS_PER_MONTH = STEPS_PER_YEAR / 12
52
+ STEPS_PER_MONTH = int(STEPS_PER_YEAR / 12)
49
53
 
50
- steps_per_unit = {
54
+ steps_per_unit: dict[TimeUnit, int] = {
51
55
  TimeUnit.SECONDS: STEPS_PER_SECOND,
52
56
  TimeUnit.MINUTES: STEPS_PER_MINUTE,
53
57
  TimeUnit.HOURS: STEPS_PER_HOUR,
@@ -70,7 +74,19 @@ class FameTime:
70
74
 
71
75
  @staticmethod
72
76
  def convert_datetime_to_fame_time_step(datetime_string: str) -> int:
73
- """Converts real Datetime string to FAME time step"""
77
+ """
78
+ Converts Datetime string to FAME time step
79
+
80
+ Args:
81
+ datetime_string: a datetime in FAME formatting
82
+
83
+ Returns:
84
+ corresponding FAME time step
85
+
86
+ Raises:
87
+ ConversionError: if string could not be interpreted or does not represent a valid FAME time step,
88
+ logged with level "ERROR"
89
+ """
74
90
  if not FameTime.is_datetime(datetime_string):
75
91
  raise log_error(ConversionError(FameTime._FORMAT_INVALID.format(datetime_string)))
76
92
  datetime = FameTime._convert_to_datetime(datetime_string)
@@ -85,7 +101,18 @@ class FameTime:
85
101
 
86
102
  @staticmethod
87
103
  def _convert_to_datetime(datetime_string: str) -> dt.datetime:
88
- """Converts given `datetime_string` to real-world datetime"""
104
+ """
105
+ Converts given `datetime_string` in FAME formatting to real-world datetime
106
+
107
+ Args:
108
+ datetime_string: to be converted to datetime
109
+
110
+ Returns:
111
+ datetime representation of provided string
112
+
113
+ Raises:
114
+ ConversionError: if provided string could not be converted, logged with level "ERROR"
115
+ """
89
116
  try:
90
117
  return dt.datetime.strptime(datetime_string, DATE_FORMAT)
91
118
  except ValueError as e:
@@ -94,8 +121,17 @@ class FameTime:
94
121
  @staticmethod
95
122
  def convert_fame_time_step_to_datetime(fame_time_steps: int, date_format: str = DATE_FORMAT) -> str:
96
123
  """
97
- Converts given `fame_time_steps` to corresponding real-world datetime string in `date_format`,
98
- raises ConversionException if invalid `date_format` received.
124
+ Converts given `fame_time_steps` to corresponding real-world datetime string in `date_format`
125
+
126
+ Args:
127
+ fame_time_steps: an integer representing time in FAME's internal format
128
+ date_format: to be used for datetime representation
129
+
130
+ Returns:
131
+ string representing the real-world datetime of the provided time steps
132
+
133
+ Raises:
134
+ ConversionError: if `date_format` is invalid, logged with level "ERROR"
99
135
  """
100
136
  years_since_start_time = math.floor(fame_time_steps / Constants.STEPS_PER_YEAR)
101
137
  current_year = years_since_start_time + Constants.FIRST_YEAR
@@ -110,21 +146,33 @@ class FameTime:
110
146
 
111
147
  @staticmethod
112
148
  def convert_time_span_to_fame_time_steps(value: int, unit: TimeUnit) -> int:
113
- """Converts value of `TimeUnit.UNIT` to fame time steps"""
149
+ """
150
+ Converts value of `TimeUnit.UNIT` to FAME time steps
151
+
152
+ Args:
153
+ value: amount of the units to be converted
154
+ unit: base time unit
155
+
156
+ Returns:
157
+ FAME time steps equivalent of `value x unit`
158
+
159
+ Raises:
160
+ ConversionError: if an unknown time unit is used, logged with level "ERROR"
161
+ """
114
162
  steps = Constants.steps_per_unit.get(unit)
115
163
  if steps:
116
164
  return steps * value
117
165
  raise log_error(ConversionError(FameTime._TIME_UNIT_UNKNOWN.format(unit)))
118
166
 
119
167
  @staticmethod
120
- def is_datetime(string: str) -> bool:
168
+ def is_datetime(string: Any) -> bool:
121
169
  """Returns `True` if given `string` matches Datetime string format and can be converted to FAME time step"""
122
170
  if isinstance(string, str):
123
171
  return DATE_REGEX.fullmatch(string.strip()) is not None
124
172
  return False
125
173
 
126
174
  @staticmethod
127
- def is_fame_time_compatible(value: Union[int, str]) -> bool:
175
+ def is_fame_time_compatible(value: int | str) -> bool:
128
176
  """Returns `True` if given int or string `value` can be converted to a FAME time step"""
129
177
  if isinstance(value, int):
130
178
  return True
@@ -142,14 +190,23 @@ class FameTime:
142
190
  return True
143
191
 
144
192
  @staticmethod
145
- def convert_string_if_is_datetime(value: Union[int, str]) -> int:
193
+ def convert_string_if_is_datetime(value: int | str) -> int:
146
194
  """
147
- Returns FAME time steps If given `value` is a valid FAME datetime string it is converted to FAME time steps;
148
- Or, if given `value` is an integer, it is returned without modification.
149
- Raises an Exception if given `value` is neither a valid FAME datetime string nor an integer value
195
+ Returns FAME time steps of given `value`. If it is a valid FAME datetime string it is converted to
196
+ FAME time steps, or, if given `value` is an integer, it is returned without modification.
197
+
198
+ Args:
199
+ value: to be converted
200
+
201
+ Returns:
202
+ FAME time steps equivalent of provided value
203
+
204
+ Raises:
205
+ ConversionError: if given `value` is neither a FAME datetime string nor an integer value,
206
+ logged with level "ERROR"
150
207
  """
151
208
  if FameTime.is_datetime(value):
152
- return int(FameTime.convert_datetime_to_fame_time_step(value))
209
+ return int(FameTime.convert_datetime_to_fame_time_step(value)) # type: ignore[arg-type]
153
210
  try:
154
211
  return int(value)
155
212
  except ValueError as e:
fameio/tools.py CHANGED
@@ -1,8 +1,10 @@
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
  from pathlib import Path
5
- from typing import Any, Union
7
+ from typing import Any
6
8
 
7
9
 
8
10
  def keys_to_lower(dictionary: dict[str, Any]) -> dict[str, Any]:
@@ -17,12 +19,12 @@ def ensure_is_list(value: Any) -> list:
17
19
  return [value]
18
20
 
19
21
 
20
- def ensure_path_exists(path: Union[Path, str]):
22
+ def ensure_path_exists(path: Path | str):
21
23
  """Creates a specified path if not already existent"""
22
24
  Path(path).mkdir(parents=True, exist_ok=True)
23
25
 
24
26
 
25
27
  def clean_up_file_name(name: str) -> str:
26
- """Returns given `name` with replacements defined in `replace_map`"""
27
- replace_map = {" ": "_", ":": "_", "/": "-"}
28
- return name.translate(str.maketrans(replace_map))
28
+ """Returns given `name` replacing spaces and colons with underscore, and slashed with a dash"""
29
+ translation_table = str.maketrans({" ": "_", ":": "_", "/": "-"})
30
+ return name.translate(translation_table)
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: fameio
3
- Version: 3.1.1
3
+ Version: 3.2.0
4
4
  Summary: Tools for input preparation and output digestion of FAME models
5
- Home-page: https://gitlab.com/fame-framework/wiki/-/wikis/home
6
5
  License: Apache-2.0
7
6
  Keywords: FAME,fameio,agent-based modelling,energy systems
8
7
  Author: Felix Nitsch
@@ -20,24 +19,28 @@ Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Topic :: Scientific/Engineering
24
- Requires-Dist: fameprotobuf (>=2.0.2,<3.0.0)
24
+ Requires-Dist: fameprotobuf (>=2.0.2,<3.0)
25
25
  Requires-Dist: pandas (>=1.0,<3.0)
26
26
  Requires-Dist: pyyaml (>=6.0,<7.0)
27
- Project-URL: Repository, https://gitlab.com/fame-framework/fame-io/
27
+ Project-URL: Changelog, https://gitlab.com/fame-framework/fame-io/-/blob/main/CHANGELOG.md
28
+ Project-URL: Homepage, https://helmholtz.software/software/fame
29
+ Project-URL: Issue Tracking, https://gitlab.com/fame-framework/fame-io/-/issues
30
+ Project-URL: Repository, https://gitlab.com/fame-framework/fame-io
28
31
  Description-Content-Type: text/markdown
29
32
 
30
33
  <!-- SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
31
34
 
32
35
  SPDX-License-Identifier: Apache-2.0 -->
33
36
 
34
- | | |
35
- |---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
36
- | **Package** | [![PyPI version](https://badge.fury.io/py/fameio.svg)](https://badge.fury.io/py/fameio) [![PyPI license](https://img.shields.io/pypi/l/fameio.svg)](https://badge.fury.io/py/fameio) [![REUSE status](https://api.reuse.software/badge/gitlab.com/fame-framework/fame-io)](https://api.reuse.software/info/gitlab.com/fame-framework/fame-io) |
37
- | **Tests** | [![pipeline status](https://gitlab.com/fame-framework/fame-io/badges/main/pipeline.svg)](https://gitlab.com/fame-framework/fame-io/commits/main) [![coverage report](https://gitlab.com/fame-framework/fame-io/badges/main/coverage.svg)](https://gitlab.com/fame-framework/fame-io/-/commits/main) |
38
- | **Activity** | ![GitLab last commit](https://img.shields.io/gitlab/last-commit/fame-framework%2Ffame-io) ![GitLab closed issues by-label](https://img.shields.io/gitlab/issues/closed/fame-framework%2Ffame-io) |
39
- | **Style** | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org) [![linting: pylint](https://img.shields.io/badge/linting-pylint-green)](https://github.com/pylint-dev/pylint) |
40
- | **Reference** | [![JOSS](https://joss.theoj.org/papers/10.21105/joss.04958/status.svg)](https://doi.org/10.21105/joss.04958) [![Zenodo](https://zenodo.org/badge/DOI/10.5281/zenodo.4314337.svg)](https://doi.org/10.5281/zenodo.4314337) |
37
+ | | |
38
+ |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
39
+ | **Package** | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fameio) ![PyPI - Version](https://img.shields.io/pypi/v/fameio) [![PyPI license](https://img.shields.io/pypi/l/fameio.svg)](https://badge.fury.io/py/fameio) [![REUSE status](https://api.reuse.software/badge/gitlab.com/fame-framework/fame-io)](https://api.reuse.software/info/gitlab.com/fame-framework/fame-io) |
40
+ | **Test** | [![pipeline status](https://gitlab.com/fame-framework/fame-io/badges/main/pipeline.svg)](https://gitlab.com/fame-framework/fame-io/commits/main) [![coverage report](https://gitlab.com/fame-framework/fame-io/badges/main/coverage.svg)](https://gitlab.com/fame-framework/fame-io/-/commits/main) |
41
+ | **Activity** | ![GitLab last commit](https://img.shields.io/gitlab/last-commit/fame-framework%2Ffame-io) ![GitLab closed issues by-label](https://img.shields.io/gitlab/issues/closed/fame-framework%2Ffame-io) |
42
+ | **Style** | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![log style - common changelog](https://img.shields.io/badge/log_style-common_changelog-blue)](https://common-changelog.org/) ![Static Badge](https://img.shields.io/badge/type%20checked-mypy-039dfc) [![linting: pylint](https://img.shields.io/badge/linting-pylint-green)](https://github.com/pylint-dev/pylint) |
43
+ | **Reference** | [![JOSS](https://joss.theoj.org/papers/10.21105/joss.04958/status.svg)](https://doi.org/10.21105/joss.04958) [![Zenodo](https://zenodo.org/badge/DOI/10.5281/zenodo.4314337.svg)](https://doi.org/10.5281/zenodo.4314337) |
41
44
 
42
45
  # FAME-Io
43
46
 
@@ -362,7 +365,9 @@ Agent Parameters:
362
365
  * `Attributes` Optional; if the agent has any attributes, specify them here in the format "AttributeName: value"; please
363
366
  see attribute table above
364
367
  * `Metadata` Optional; can be assigned to each instance of an Agent, as well as to each of its Attributes
368
+ * `Ext` Optional; Reserved key for parameters not used by fameio but its extensions, e.g., FAME-Gui
365
369
 
370
+ A warning is logged for any other key at this level.
366
371
  The specified `Attributes` for each agent must match the specified `Attributes` options in the linked Schema (see
367
372
  above).
368
373
  For better structure and readability of the `scenario.yaml`, `Attributes` may also be specified in a nested way as
@@ -642,7 +647,7 @@ These CSV files follow a specific structure:
642
647
  * They should contain exactly two columns - any other columns are ignored.
643
648
  A warning is raised if more than two non-empty columns are detected.
644
649
  * The first column must be a time stamp in form `YYYY-MM-DD_hh:mm:ss` or
645
- a [FAME-Timestamp](https://gitlab.com/fame-framework/wiki/-/wikis/architecture/decisions/TimeStamp) integer value
650
+ a [FAME-Timestamp](https://gitlab.com/fame-framework/wiki/-/wikis/architecture/decisions/TimeStamp) integer value.
646
651
  * The second column must be a numerical value (either integer or floating-point)
647
652
  * The separator of the two columns is a semicolon
648
653
  * The data must **not** have headers, except for comments marked with `#`
@@ -662,6 +667,7 @@ Please refer also to the detailed article about `TimeStamps` in
662
667
  the [FAME-Wiki](https://gitlab.com/fame-framework/wiki/-/wikis/TimeStamp).
663
668
  For large CSV files (with more than 20,000 rows) we recommend using the integer representation of FAME-Timestamps in the
664
669
  first column (instead of text representation) to improve conversion speed.
670
+ A warning will be raised for very large files (exceeding 50,000 rows) that require time stamp conversion.
665
671
 
666
672
  ### Split and join multiple YAML files
667
673