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