fram-core 0.0.0__py3-none-any.whl → 0.1.0a1__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 (103) hide show
  1. fram_core-0.1.0a1.dist-info/METADATA +41 -0
  2. fram_core-0.1.0a1.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0a1.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0a1.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +142 -0
  6. framcore/Model.py +73 -0
  7. framcore/__init__.py +9 -0
  8. framcore/aggregators/Aggregator.py +153 -0
  9. framcore/aggregators/HydroAggregator.py +837 -0
  10. framcore/aggregators/NodeAggregator.py +495 -0
  11. framcore/aggregators/WindSolarAggregator.py +323 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +305 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +37 -0
  17. framcore/attributes/SoftBound.py +19 -0
  18. framcore/attributes/StartUpCost.py +54 -0
  19. framcore/attributes/Storage.py +146 -0
  20. framcore/attributes/TargetBound.py +18 -0
  21. framcore/attributes/__init__.py +65 -0
  22. framcore/attributes/hydro/HydroBypass.py +42 -0
  23. framcore/attributes/hydro/HydroGenerator.py +83 -0
  24. framcore/attributes/hydro/HydroPump.py +156 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +714 -0
  28. framcore/components/Component.py +112 -0
  29. framcore/components/Demand.py +130 -0
  30. framcore/components/Flow.py +167 -0
  31. framcore/components/HydroModule.py +330 -0
  32. framcore/components/Node.py +76 -0
  33. framcore/components/Thermal.py +204 -0
  34. framcore/components/Transmission.py +183 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +67 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +155 -0
  40. framcore/curves/__init__.py +9 -0
  41. framcore/events/__init__.py +21 -0
  42. framcore/events/events.py +51 -0
  43. framcore/expressions/Expr.py +490 -0
  44. framcore/expressions/__init__.py +28 -0
  45. framcore/expressions/_get_constant_from_expr.py +483 -0
  46. framcore/expressions/_time_vector_operations.py +615 -0
  47. framcore/expressions/_utils.py +73 -0
  48. framcore/expressions/queries.py +423 -0
  49. framcore/expressions/units.py +207 -0
  50. framcore/fingerprints/__init__.py +11 -0
  51. framcore/fingerprints/fingerprint.py +293 -0
  52. framcore/juliamodels/JuliaModel.py +161 -0
  53. framcore/juliamodels/__init__.py +7 -0
  54. framcore/loaders/__init__.py +10 -0
  55. framcore/loaders/loaders.py +407 -0
  56. framcore/metadata/Div.py +73 -0
  57. framcore/metadata/ExprMeta.py +50 -0
  58. framcore/metadata/LevelExprMeta.py +17 -0
  59. framcore/metadata/Member.py +55 -0
  60. framcore/metadata/Meta.py +44 -0
  61. framcore/metadata/__init__.py +15 -0
  62. framcore/populators/Populator.py +108 -0
  63. framcore/populators/__init__.py +7 -0
  64. framcore/querydbs/CacheDB.py +50 -0
  65. framcore/querydbs/ModelDB.py +34 -0
  66. framcore/querydbs/QueryDB.py +45 -0
  67. framcore/querydbs/__init__.py +11 -0
  68. framcore/solvers/Solver.py +48 -0
  69. framcore/solvers/SolverConfig.py +272 -0
  70. framcore/solvers/__init__.py +9 -0
  71. framcore/timeindexes/AverageYearRange.py +20 -0
  72. framcore/timeindexes/ConstantTimeIndex.py +17 -0
  73. framcore/timeindexes/DailyIndex.py +21 -0
  74. framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
  75. framcore/timeindexes/HourlyIndex.py +21 -0
  76. framcore/timeindexes/IsoCalendarDay.py +31 -0
  77. framcore/timeindexes/ListTimeIndex.py +197 -0
  78. framcore/timeindexes/ModelYear.py +17 -0
  79. framcore/timeindexes/ModelYears.py +18 -0
  80. framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
  81. framcore/timeindexes/ProfileTimeIndex.py +32 -0
  82. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  83. framcore/timeindexes/TimeIndex.py +90 -0
  84. framcore/timeindexes/WeeklyIndex.py +21 -0
  85. framcore/timeindexes/__init__.py +36 -0
  86. framcore/timevectors/ConstantTimeVector.py +135 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +114 -0
  88. framcore/timevectors/ListTimeVector.py +123 -0
  89. framcore/timevectors/LoadedTimeVector.py +104 -0
  90. framcore/timevectors/ReferencePeriod.py +41 -0
  91. framcore/timevectors/TimeVector.py +94 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +36 -0
  94. framcore/utils/get_regional_volumes.py +369 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +46 -0
  97. framcore/utils/isolate_subnodes.py +163 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +107 -0
  101. fram_core-0.0.0.dist-info/METADATA +0 -5
  102. fram_core-0.0.0.dist-info/RECORD +0 -4
  103. fram_core-0.0.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,407 @@
1
+ """Classes defining APIs for Loaders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from pathlib import Path
7
+ from typing import ClassVar
8
+
9
+ from numpy.typing import NDArray
10
+
11
+ from framcore import Base
12
+ from framcore.fingerprints import Fingerprint
13
+ from framcore.timeindexes import TimeIndex
14
+ from framcore.timevectors import ReferencePeriod
15
+
16
+
17
+ class Loader(Base, ABC):
18
+ """Base Loader class defining common API and functionality for all Loaders."""
19
+
20
+ def __init__(self) -> None:
21
+ """Set up cache of ids contained in the source of the Loader."""
22
+ self._content_ids: list[str] = None
23
+
24
+ def __repr__(self) -> str:
25
+ """
26
+ Overwrite string representation.
27
+
28
+ Returns:
29
+ str: Object represented as string.
30
+
31
+ """
32
+ return f"{type(self).__name__}({vars(self)})"
33
+
34
+ def __getstate__(self) -> dict:
35
+ """
36
+ Return current object state, clearing any cached data.
37
+
38
+ Returns:
39
+ dict: The object's state dictionary.
40
+
41
+ """
42
+ self.clear_cache()
43
+ return self.__dict__
44
+
45
+ @abstractmethod
46
+ def clear_cache(self) -> None:
47
+ """Clear cached data from the loader."""
48
+ pass
49
+
50
+ # TODO: Is this correct? Also figure out how sharing Loaders should be when copying model given filepaths and copied
51
+ # database
52
+ def __deepcopy__(self, memo: dict) -> Loader:
53
+ """
54
+ Overwrite deepcopy.
55
+
56
+ This is done to enable sharing of loaders. Since a loader is connected to one source, caching can thus be shared
57
+ between Models.
58
+
59
+ Args:
60
+ memo (dict): Required argument.
61
+
62
+ Returns:
63
+ Loader: Returns itself.
64
+
65
+ """
66
+ return self
67
+
68
+ @abstractmethod
69
+ def get_source(self) -> object:
70
+ """
71
+ Return Loader source.
72
+
73
+ Returns:
74
+ object: Whatever the Loader interacts with to retrieve data.
75
+
76
+ """
77
+ pass
78
+
79
+ @abstractmethod
80
+ def set_source(self, new_source: object) -> None:
81
+ """
82
+ Set the Loader source.
83
+
84
+ Args:
85
+ new_source (object): Whatever the Loader should interact with to retrieve data.
86
+
87
+ """
88
+ pass
89
+
90
+ @abstractmethod
91
+ def get_metadata(self, content_id: str) -> object:
92
+ """
93
+ Get metadata from the Loader source.
94
+
95
+ The metadata could describe behavior of the data in source.
96
+
97
+ Args:
98
+ content_id (str): Id of some content.
99
+
100
+ Returns:
101
+ object: Metadata in some format only the specific Loader knows.
102
+
103
+ """
104
+ pass
105
+
106
+ @abstractmethod
107
+ def _get_ids(self) -> list[str]:
108
+ """
109
+ Return list of names which can be used to access specific data structures whithin source.
110
+
111
+ Most likely the names of all time vectors or curves in The Loader's source.
112
+
113
+ Returns:
114
+ list[str]
115
+
116
+ """
117
+ pass
118
+
119
+ def get_ids(self) -> list[str]:
120
+ """
121
+ Handle caching of ids existing in the loaders source.
122
+
123
+ Returns:
124
+ list[str]: List containing ids in Loader source.
125
+
126
+ """
127
+ if self._content_ids is None:
128
+ self._content_ids = self._get_ids()
129
+ seen = set()
130
+ duplicates = []
131
+ for content_id in self._content_ids:
132
+ if content_id in seen:
133
+ duplicates.append(content_id)
134
+ else:
135
+ seen.add(content_id)
136
+ if duplicates:
137
+ msg = f"Duplicate ID's found in {self.get_source()}: {duplicates}"
138
+ raise ValueError(msg)
139
+
140
+ return self._content_ids
141
+
142
+ def _id_exsists(self, content_id: str) -> None:
143
+ """
144
+ Check if a given id exists in source.
145
+
146
+ Args:
147
+ content_id (str): Id of some content.
148
+
149
+ Raises:
150
+ KeyError: If content id does not exist.
151
+
152
+ """
153
+ existing_ids = self.get_ids()
154
+ if content_id not in existing_ids:
155
+ # __repr__ should be overwritten in subclasses to produce enough info in error message.
156
+ msg = f"Could not find ID {content_id} in {self}. Existing IDs: {existing_ids}"
157
+ raise KeyError(msg)
158
+
159
+
160
+ class TimeVectorLoader(Loader, ABC):
161
+ """Loader API for retrieving time vector data from some source."""
162
+
163
+ @abstractmethod
164
+ def get_values(self, vector_id: str) -> NDArray:
165
+ """
166
+ Return the values of a time vector in the Loader source.
167
+
168
+ Args:
169
+ vector_id (str): ID of the vector.
170
+
171
+ Returns:
172
+ NDArray: Numpy array of all values.
173
+
174
+ """
175
+ pass
176
+
177
+ @abstractmethod
178
+ def get_index(self, vector_id: str) -> TimeIndex:
179
+ """
180
+ Return the index a time vector in the Loader source.
181
+
182
+ Args:
183
+ vector_id (str): ID of the vector.
184
+
185
+ Returns:
186
+ NDArray: TimeIndex object.
187
+
188
+ """
189
+ pass
190
+
191
+ @abstractmethod
192
+ def get_unit(self, vector_id: str) -> str:
193
+ """
194
+ Return unit of the values within a time vector in the loader source.
195
+
196
+ Args:
197
+ vector_id (str): ID of the vector.
198
+
199
+ Returns:
200
+ str: String with unit.
201
+
202
+ """
203
+ pass
204
+
205
+ @abstractmethod
206
+ def is_max_level(self, vector_id: str) -> bool | None:
207
+ """
208
+ Check if the given TimeVector is a level representing max Volume/Capacity/Price.
209
+
210
+ Args:
211
+ vector_id (str): ID of the vector.
212
+
213
+ Returns:
214
+ True - vector is a level representing max Volume/Capacity.
215
+ False - vector is a level representing average Volume/Capacity over a given reference period.
216
+ None - vector is not a level.
217
+
218
+ """
219
+ pass
220
+
221
+ @abstractmethod
222
+ def is_zero_one_profile(self, vector_id: str) -> bool | None:
223
+ """
224
+ Check if the given TimeVector is a profile with values between zero and one.
225
+
226
+ Args:
227
+ vector_id (str): ID of the vector.
228
+
229
+ Returns:
230
+ True - vector is a profile with values between zero and one.
231
+ False - vector is a profile where the mean value is 1 given a reference period.
232
+ None - vector is not a profile.
233
+
234
+ """
235
+ pass
236
+
237
+ @abstractmethod
238
+ def get_reference_period(self, vector_id: str) -> ReferencePeriod | None:
239
+ """
240
+ Get the reference period of a given vector.
241
+
242
+ Args:
243
+ vector_id (str): ID of the vector.
244
+
245
+ Returns:
246
+ ReferencePeriod - if the vector is a mean one profile or average level, a reference period must exist.
247
+ None - No reference period if vector is max level, zero one profile or not a level or profile.
248
+
249
+ """
250
+ pass
251
+
252
+ def get_fingerprint(self, vector_id: str) -> Fingerprint:
253
+ """Return Loader Fingerprint for given vector id."""
254
+ f = Fingerprint(self)
255
+ f.add("unit", self.get_unit(vector_id))
256
+ f.add("index", self.get_index(vector_id))
257
+ f.add("values", self.get_values(vector_id))
258
+ return f
259
+
260
+
261
+ class CurveLoader(Loader, ABC):
262
+ """Loader API for retrieving curve data from some source."""
263
+
264
+ @abstractmethod
265
+ def get_y_axis(self, curve_id: str) -> NDArray:
266
+ """
267
+ Return the values of a Curves y axis in the Loader source.
268
+
269
+ Args:
270
+ curve_id (str): ID of the curve.
271
+
272
+ Returns:
273
+ NDArray: Numpy array of all values in the y axis.
274
+
275
+ """
276
+ pass
277
+
278
+ @abstractmethod
279
+ def get_x_axis(self, curve_id: str) -> NDArray:
280
+ """
281
+ Return the values of a Curves x axis in the Loader source.
282
+
283
+ Args:
284
+ curve_id (str): ID of the curve.
285
+
286
+ Returns:
287
+ NDArray: Numpy array of all values in the x axis.
288
+
289
+ """
290
+ pass
291
+
292
+ @abstractmethod
293
+ def get_x_unit(self, curve_id: str) -> str:
294
+ """
295
+ Return the unit of the x axis of a specific curve.
296
+
297
+ Args:
298
+ curve_id (str): ID of the curve.
299
+
300
+ Returns:
301
+ str: Unit of the curve's x axis.
302
+
303
+ """
304
+ pass
305
+
306
+ @abstractmethod
307
+ def get_y_unit(self, curve_id: str) -> str:
308
+ """
309
+ Return the unit of the y axis of a specific curve.
310
+
311
+ Args:
312
+ curve_id (str): ID of the curve.
313
+
314
+ Returns:
315
+ str: Unit of the curve's y axis.
316
+
317
+ """
318
+ pass
319
+
320
+
321
+ class FileLoader(Loader, ABC):
322
+ """Define common functionality and API for Loaders connected to a file as source."""
323
+
324
+ _SUPPORTED_SUFFIXES: ClassVar[list[str]] = []
325
+
326
+ def __init__(self, source: Path | str, relative_loc: Path | str | None = None) -> None:
327
+ """
328
+ Check validity of input parameters.
329
+
330
+ Args:
331
+ source (Path | str): Full file path or the absolute part of a file path
332
+ relative_loc (Optional[Union[Path, str]], optional): The relative part of a file path. Defaults to None.
333
+
334
+ """
335
+ super().__init__()
336
+ self._source = source
337
+ self._relative_loc = relative_loc
338
+
339
+ self._check_type(source, (Path, str))
340
+ if self._relative_loc is not None:
341
+ self._check_type(self._relative_loc, (Path, str))
342
+ self._check_path_exists(self.get_source())
343
+ self._check_path_supported(self.get_source())
344
+
345
+ def __repr__(self) -> str:
346
+ """Overwrite __repr__ to get better info."""
347
+ return f"{type(self).__name__}(source={self._source}, relative_loc={self._relative_loc})"
348
+
349
+ def get_source(self) -> Path:
350
+ """Combine absolute and relative file path (if relative is defined) to get full source."""
351
+ if self._relative_loc is None:
352
+ return Path(self._source)
353
+ return Path(self._source) / self._relative_loc
354
+
355
+ def set_source(self, new_source: Path, relative_loc: Path | str | None = None) -> None:
356
+ """
357
+ Set absolute and relative parts of filepath.
358
+
359
+ Args:
360
+ new_source (Path): New absolute part.
361
+ relative_loc (Optional[Union[Path, str]], optional): New relative part. Defaults to None.
362
+
363
+ """
364
+ self._source = new_source
365
+ self._relative_loc = relative_loc
366
+
367
+ @classmethod
368
+ def get_supported_suffixes(cls) -> list[str]:
369
+ """
370
+ Return list of supported file types.
371
+
372
+ Returns:
373
+ list: List of filetypes.
374
+
375
+ """
376
+ return cls._SUPPORTED_SUFFIXES
377
+
378
+ def _check_path_exists(self, path: Path) -> None:
379
+ """
380
+ Check if a file path exists.
381
+
382
+ Args:
383
+ path (Path): Path to check.
384
+
385
+ Raises:
386
+ FileNotFoundError
387
+
388
+ """
389
+ if not path.exists():
390
+ msg = f"""File {path} does not exist. Could not create {type(self)}."""
391
+ raise FileNotFoundError(msg)
392
+
393
+ def _check_path_supported(self, path: Path) -> None:
394
+ """
395
+ Check if a file is supported/readable by this FileLoader instance.
396
+
397
+ Args:
398
+ path (Path): Path to check.
399
+
400
+ Raises:
401
+ ValueError: If the file type is not defined as supported.
402
+
403
+ """
404
+ filetype = path.suffix
405
+ if filetype not in self._SUPPORTED_SUFFIXES:
406
+ msg = f"File type of {path}, {filetype} is not supported by {type(self)}. Supported filetypes: {self._SUPPORTED_SUFFIXES}"
407
+ raise ValueError(msg)
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from framcore.fingerprints import Fingerprint
4
+ from framcore.fingerprints.fingerprint import _custom_hash # TODO: is this needed?
5
+ from framcore.metadata.Meta import Meta # NB! full import path needed for inheritance to work
6
+
7
+
8
+ class Div(Meta):
9
+ """
10
+ Div class is made for loss-less aggregation of metadata. Subclass of Meta.
11
+
12
+ It's combine method is made to keep all unique metadata,
13
+ so that nothing is thrown away in connection with aggregation.
14
+ """
15
+
16
+ def __init__(self, value: Meta | set[Meta] | None = None) -> None:
17
+ """Create Div metadata."""
18
+ self._check_type_meta(value, with_none=True)
19
+
20
+ self._value: set[Meta] = set()
21
+
22
+ if isinstance(value, set):
23
+ self._value.update(value)
24
+
25
+ elif isinstance(value, Meta):
26
+ self._value.add(value)
27
+
28
+ def _check_type_meta(self, value: Meta | set[Meta], with_none: bool) -> None:
29
+ if with_none:
30
+ self._check_type(value, (Meta, set, type(None)))
31
+ else:
32
+ self._check_type(value, (Meta, set))
33
+ if isinstance(value, set):
34
+ for x in value:
35
+ self._check_type(x, Meta)
36
+
37
+ def get_value(self) -> set[Meta]:
38
+ """Return str value."""
39
+ return self._value
40
+
41
+ def set_value(self, value: Meta | set[Meta]) -> None:
42
+ """Set str value. TypeError if not str."""
43
+ self._check_type_meta(value, with_none=False)
44
+ if isinstance(value, set):
45
+ self._value.update(value)
46
+
47
+ elif isinstance(value, Meta):
48
+ self._value.add(value)
49
+
50
+ def combine(self, other: Meta | set[Meta]) -> Div:
51
+ """Just consume other and return self."""
52
+ self._check_type_meta(other, with_none=True)
53
+ if isinstance(other, Div):
54
+ for x in other.get_value():
55
+ self.combine(x)
56
+ else:
57
+ self.set_value(other)
58
+ return self
59
+
60
+ def get_fingerprint(self) -> Fingerprint:
61
+ """
62
+ Generate and return a Fingerprint representing the current set of Meta values.
63
+
64
+ Returns
65
+ -------
66
+ Fingerprint
67
+ A fingerprint object based on the hashes of the contained Meta values.
68
+
69
+ """
70
+ fingerprint = Fingerprint()
71
+ hash_list = [value.get_fingerprint().get_hash() for value in self._value]
72
+ fingerprint.add("_value", _custom_hash(hash_list))
73
+ return fingerprint
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from framcore.expressions import Expr
4
+ from framcore.fingerprints import Fingerprint
5
+ from framcore.metadata import Div
6
+ from framcore.metadata.Meta import Meta # NB! full import path needed for inheritance to work
7
+
8
+
9
+ class ExprMeta(Meta):
10
+ """
11
+ ExprMeta represent an Expr. Subclass of Meta.
12
+
13
+ When used, all components must have a ExprMeta.
14
+ """
15
+
16
+ def __init__(self, value: Expr) -> None:
17
+ """Create new ExprMeta with float value."""
18
+ self._value = value
19
+ self._check_type(value, Expr)
20
+
21
+ def __repr__(self) -> str:
22
+ """Overwrite __repr__ for better string representation."""
23
+ return f"{type(self).__name__}(expr={self._value})"
24
+
25
+ def __eq__(self, other: object) -> bool:
26
+ """Check equality based on expr."""
27
+ if not isinstance(other, ExprMeta):
28
+ return False
29
+ return self._value == other._value
30
+
31
+ def get_value(self) -> float:
32
+ """Return expr."""
33
+ return self._value
34
+
35
+ def set_value(self, value: Expr) -> None:
36
+ """Set expr value. TypeError if not expr."""
37
+ self._check_type(value, Expr)
38
+ self._value = value
39
+
40
+ def combine(self, other: Meta) -> Expr | Div:
41
+ """Sum Expr."""
42
+ if isinstance(other, ExprMeta):
43
+ return Expr(self._value + other.get_value())
44
+ d = Div(self)
45
+ d.set_value(other)
46
+ return d
47
+
48
+ def get_fingerprint(self) -> Fingerprint:
49
+ """Get the fingerprint of the ScalarMeta."""
50
+ return self.get_fingerprint_default()
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from framcore.expressions import Expr, ensure_expr
4
+ from framcore.metadata.ExprMeta import ExprMeta # NB! full import path needed for inheritance to work
5
+ from framcore.timevectors import TimeVector
6
+
7
+
8
+ class LevelExprMeta(ExprMeta):
9
+ """
10
+ LevelExprMeta represent an Expr. Subclass of ExprMeta.
11
+
12
+ When used, all components must have a ExprMeta.
13
+ """
14
+
15
+ def __init__(self, value: Expr | TimeVector) -> None:
16
+ """Create new LevelExprMeta with float value."""
17
+ self._value = ensure_expr(value, is_level=True)
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from framcore.fingerprints import Fingerprint
4
+ from framcore.metadata import Div
5
+ from framcore.metadata.Meta import Meta # NB! full import path needed for inheritance to work
6
+
7
+
8
+ class Member(Meta):
9
+ """
10
+ Member represent membership to a catergory with a str. Subclass of Meta.
11
+
12
+ Should not have missing values.
13
+
14
+ When used, all components must have a membership.
15
+ """
16
+
17
+ def __init__(self, value: str | float | int) -> None: # TODO: only str
18
+ """Create new member with str value."""
19
+ self._value = value # set before checking, otherwise __repr__ can fail because self._value is not set.
20
+ self._check_type(value, (str, float, int))
21
+
22
+ def __repr__(self) -> str:
23
+ """Overwrite __repr__ for better string representation."""
24
+ return f"{type(self).__name__}(value={self._value})"
25
+
26
+ def __eq__(self, other: object) -> bool:
27
+ """Check equality based on value."""
28
+ if not isinstance(other, Member):
29
+ return False
30
+ return self._value == other._value
31
+
32
+ def __hash__(self) -> int:
33
+ """Overwrite __hash__ since its added to sets."""
34
+ return hash(self.__repr__())
35
+
36
+ def get_value(self) -> str | float | int:
37
+ """Return str value."""
38
+ return self._value
39
+
40
+ def set_value(self, value: str | float | int) -> None:
41
+ """Set str value. TypeError if not str."""
42
+ self._check_type(value, (str, float, int))
43
+ self._value = value
44
+
45
+ def combine(self, other: Meta) -> Member | Div:
46
+ """Return self if other == self else return Div containing both."""
47
+ if self == other:
48
+ return self
49
+ d = Div(self)
50
+ d.set_value(other)
51
+ return d
52
+
53
+ def get_fingerprint(self) -> Fingerprint:
54
+ """Get the fingerprint of the Member."""
55
+ return self.get_fingerprint_default()
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ from framcore import Base
7
+ from framcore.fingerprints import Fingerprint
8
+
9
+
10
+ class Meta(Base, ABC):
11
+ """
12
+ Metadata-interface class for components.
13
+
14
+ The interface is there to support validation and aggregation.
15
+ - Some types of metadata should not have any missing values
16
+ - Different types of metadata should be aggregated differently (e.g. ignore, sum, mean, keep all in list, etc.)
17
+ """
18
+
19
+ @abstractmethod
20
+ def get_value(self) -> Any: # noqa: ANN401
21
+ """Return metadata value."""
22
+ pass
23
+
24
+ @abstractmethod
25
+ def set_value(self, value: Any) -> None: # noqa: ANN401
26
+ """
27
+ Set metadata value.
28
+
29
+ Error if incorrect type or value.
30
+
31
+ Some Meta types may be immutable and thus error if
32
+ set_value is called with any value.
33
+ """
34
+ pass
35
+
36
+ @abstractmethod
37
+ def combine(self, other: Meta) -> Meta | None:
38
+ """How should this metadata type be aggregated?."""
39
+ pass
40
+
41
+ @abstractmethod
42
+ def get_fingerprint(self) -> Fingerprint:
43
+ """Return fingerprint."""
44
+ pass
@@ -0,0 +1,15 @@
1
+ # framcore/metadata/__init__.py
2
+
3
+ from framcore.metadata.Div import Div
4
+ from framcore.metadata.ExprMeta import ExprMeta
5
+ from framcore.metadata.LevelExprMeta import LevelExprMeta
6
+ from framcore.metadata.Member import Member
7
+ from framcore.metadata.Meta import Meta
8
+
9
+ __all__ = [
10
+ "Div",
11
+ "ExprMeta",
12
+ "LevelExprMeta",
13
+ "Member",
14
+ "Meta",
15
+ ]