steer-core 0.1.30__py3-none-any.whl → 0.1.33__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.
@@ -1,15 +1,20 @@
1
1
  import sqlite3 as sql
2
2
  from pathlib import Path
3
+ from typing import TypeVar
3
4
  import pandas as pd
4
5
  import importlib.resources
5
6
 
6
7
  from steer_core.Constants.Units import *
8
+ from steer_core.Mixins.Serializer import SerializerMixin
9
+
10
+
11
+ T = TypeVar('T', bound='SerializerMixin')
7
12
 
8
13
 
9
14
  class DataManager:
10
15
 
11
16
  def __init__(self):
12
- with importlib.resources.path("steer_core.Data", "database.db") as db_path:
17
+ with importlib.resources.path("steer_opencell_data", "database.db") as db_path:
13
18
  self._db_path = db_path
14
19
  self._connection = sql.connect(self._db_path)
15
20
  self._cursor = self._connection.cursor()
@@ -356,5 +361,72 @@ class DataManager:
356
361
  self._cursor.execute(f"DELETE FROM {table_name} WHERE {condition}")
357
362
  self._connection.commit()
358
363
 
364
+ @classmethod
365
+ def from_database(cls: type[T], name: str, table_name: str = None) -> T:
366
+ """
367
+ Pull object from the database by name.
368
+
369
+ Subclasses must define a '_table_name' class variable (str or list of str)
370
+ unless table_name is explicitly provided.
371
+
372
+ Parameters
373
+ ----------
374
+ name : str
375
+ Name of the object to retrieve.
376
+ table_name : str, optional
377
+ Specific table to search. If provided, '_table_name' is not required.
378
+ If None, uses class's _table_name.
379
+
380
+ Returns
381
+ -------
382
+ T
383
+ Instance of the class.
384
+
385
+ Raises
386
+ ------
387
+ NotImplementedError
388
+ If the subclass doesn't define '_table_name' and table_name is not provided.
389
+ ValueError
390
+ If the object name is not found in any of the tables.
391
+ """
392
+ database = cls()
393
+
394
+ # Get list of tables to search
395
+ if table_name:
396
+ tables_to_search = [table_name]
397
+ else:
398
+ # Only check for _table_name if table_name wasn't provided
399
+ if not hasattr(cls, '_table_name'):
400
+ raise NotImplementedError(
401
+ f"{cls.__name__} must define a '_table_name' class variable "
402
+ "or provide 'table_name' argument"
403
+ )
404
+
405
+ if isinstance(cls._table_name, (list, tuple)):
406
+ tables_to_search = cls._table_name
407
+ else:
408
+ tables_to_search = [cls._table_name]
409
+
410
+ # Try each table until found
411
+ for table in tables_to_search:
412
+ available_materials = database.get_unique_values(table, "name")
413
+
414
+ if name in available_materials:
415
+ data = database.get_data(table, condition=f"name = '{name}'")
416
+ serialized_bytes = data["object"].iloc[0]
417
+ return cls.deserialize(serialized_bytes)
418
+
419
+ # Not found in any table
420
+ all_available = []
421
+ for table in tables_to_search:
422
+ all_available.extend(database.get_unique_values(table, "name"))
423
+
424
+ raise ValueError(
425
+ f"'{name}' not found in tables {tables_to_search}. "
426
+ f"Available: {all_available}"
427
+ )
428
+
359
429
  def __del__(self):
360
430
  self._connection.close()
431
+
432
+
@@ -154,7 +154,10 @@ class DunderMixin:
154
154
  """
155
155
  String representation of the instance showing all @property decorated attributes and their values.
156
156
  """
157
- return f"{self.__class__.__name__}, {self.__name__}"
157
+ if hasattr(self, 'name') and self.name:
158
+ return f"{self.__class__.__name__} ({self.name})"
159
+ else:
160
+ return f"{self.__class__.__name__}"
158
161
 
159
162
  def __repr__(self):
160
163
  """
@@ -366,3 +366,5 @@ class PlotterMixin:
366
366
 
367
367
 
368
368
 
369
+
370
+
@@ -1,46 +1,298 @@
1
- import base64
2
- from pickle import loads, dumps
3
- from typing import Type
4
- from copy import deepcopy
1
+ import msgpack
2
+ import msgpack_numpy as m
3
+ import zlib
4
+ from typing import TypeVar, Any
5
+ from datetime import datetime
6
+ from enum import Enum
7
+
8
+ T = TypeVar('T', bound='SerializerMixin')
5
9
 
6
10
 
7
11
  class SerializerMixin:
8
12
 
9
- def serialize(self) -> str:
13
+ def serialize(self, compress: bool = True) -> bytes:
10
14
  """
11
- Serialize an object to a string representation.
15
+ Serialize object using MessagePack with numpy support.
12
16
 
13
17
  Parameters
14
18
  ----------
15
- obj : Type
16
- The object to serialize.
19
+ compress : bool, optional
20
+ Whether to compress the serialized data (default: True)
17
21
 
18
22
  Returns
19
23
  -------
20
- str
21
- The serialized string representation of the object.
24
+ bytes
25
+ The serialized byte representation of the object.
22
26
  """
23
- pickled = dumps(self)
24
- based = base64.b64encode(pickled).decode("utf-8")
25
- return based
26
-
27
- @staticmethod
28
- def deserialize(String: str) -> Type:
27
+ m.patch() # Enable numpy support
28
+ # Include class information for proper deserialization
29
+ obj_dict = {
30
+ '_class': f"{self.__class__.__module__}.{self.__class__.__name__}",
31
+ **self._to_dict()
32
+ }
33
+ data = msgpack.packb(obj_dict, use_bin_type=True)
34
+
35
+ if compress:
36
+ # Add marker byte to indicate compression
37
+ return b'\x01' + zlib.compress(data, level=6)
38
+ else:
39
+ return b'\x00' + data
40
+
41
+ def _serialize_value(self, value: Any) -> Any:
42
+ """
43
+ Recursively serialize a value, handling nested structures.
44
+
45
+ Parameters
46
+ ----------
47
+ value : Any
48
+ The value to serialize.
49
+
50
+ Returns
51
+ -------
52
+ Any
53
+ The serialized representation.
54
+ """
55
+ if hasattr(value, '_to_dict'):
56
+ # Add marker and class info for object reconstruction
57
+ return {
58
+ '__object__': True,
59
+ '_class': f"{value.__class__.__module__}.{value.__class__.__name__}",
60
+ **value._to_dict()
61
+ }
62
+ elif isinstance(value, datetime):
63
+ return {'__datetime__': value.isoformat()}
64
+ elif isinstance(value, Enum):
65
+ return {
66
+ '__enum__': True,
67
+ 'class': f"{value.__class__.__module__}.{value.__class__.__name__}",
68
+ 'value': value.value
69
+ }
70
+ elif isinstance(value, tuple):
71
+ # Recursively serialize tuple items
72
+ return {
73
+ '__tuple__': True,
74
+ 'items': [self._serialize_value(item) for item in value]
75
+ }
76
+ elif isinstance(value, list):
77
+ # Recursively serialize list items
78
+ return [self._serialize_value(item) for item in value]
79
+ elif isinstance(value, dict):
80
+ # Handle dictionaries with object keys or values
81
+ has_object_keys = value and any(hasattr(k, '_to_dict') for k in value.keys())
82
+ has_object_values = value and any(hasattr(v, '_to_dict') for v in value.values())
83
+
84
+ if has_object_keys or has_object_values:
85
+ return {
86
+ '__object_dict__': True,
87
+ 'items': [
88
+ {
89
+ 'key': self._serialize_value(k),
90
+ 'value': self._serialize_value(v)
91
+ }
92
+ for k, v in value.items()
93
+ ]
94
+ }
95
+ else:
96
+ # Recursively serialize regular dict values
97
+ return {k: self._serialize_value(v) for k, v in value.items()}
98
+ else:
99
+ return value
100
+
101
+ def _to_dict(self) -> dict:
29
102
  """
30
- Deserialize a string representation into an object.
103
+ Convert object to dictionary for serialization.
104
+ Override this in subclasses to customize serialization behavior.
105
+
106
+ Returns
107
+ -------
108
+ dict
109
+ Dictionary representation of the object.
110
+ """
111
+ result = {}
112
+ for key, value in self.__dict__.items():
113
+ result[key] = self._serialize_value(value)
114
+ return result
115
+
116
+ @classmethod
117
+ def deserialize(cls: type[T], data: bytes) -> T:
118
+ """
119
+ Deserialize byte data into an object.
120
+ Automatically detects and decompresses if needed.
31
121
 
32
122
  Parameters
33
123
  ----------
34
- String : str
35
- The string representation to deserialize.
124
+ data : bytes
125
+ The byte data to deserialize.
36
126
 
37
127
  Returns
38
128
  -------
39
- SerializerMixin
129
+ T
130
+ Instance of the class.
131
+ """
132
+ m.patch() # Enable numpy support
133
+
134
+ # Check compression marker
135
+ if data[0:1] == b'\x01':
136
+ data = zlib.decompress(data[1:])
137
+ else:
138
+ data = data[1:]
139
+
140
+ obj_dict = msgpack.unpackb(data, raw=False)
141
+
142
+ # Use stored class information if available
143
+ if '_class' in obj_dict:
144
+ import importlib
145
+ module_name, class_name = obj_dict['_class'].rsplit('.', 1)
146
+ module = importlib.import_module(module_name)
147
+ actual_cls = getattr(module, class_name)
148
+ # Remove class marker before reconstructing
149
+ obj_data = {k: v for k, v in obj_dict.items() if k != '_class'}
150
+ return actual_cls._from_dict(obj_data)
151
+ else:
152
+ # Fallback for backward compatibility
153
+ return cls._from_dict(obj_dict)
154
+
155
+ @classmethod
156
+ def _deserialize_value(cls, value: Any) -> Any:
157
+ """
158
+ Recursively deserialize a value, handling nested structures.
159
+
160
+ Parameters
161
+ ----------
162
+ value : Any
163
+ The value to deserialize.
164
+
165
+ Returns
166
+ -------
167
+ Any
40
168
  The deserialized object.
41
169
  """
42
- decoded = base64.b64decode(String.encode("utf-8"))
43
- obj = deepcopy(loads(decoded))
170
+ if isinstance(value, dict):
171
+ if '__datetime__' in value:
172
+ return datetime.fromisoformat(value['__datetime__'])
173
+ elif '__enum__' in value:
174
+ # Reconstruct enum
175
+ module_name, class_name = value['class'].rsplit('.', 1)
176
+ import importlib
177
+ module = importlib.import_module(module_name)
178
+ enum_class = getattr(module, class_name)
179
+ return enum_class(value['value'])
180
+ elif '__tuple__' in value:
181
+ # Recursively reconstruct tuple items
182
+ return tuple(cls._deserialize_value(item) for item in value['items'])
183
+ elif '__object__' in value:
184
+ # Reconstruct regular object
185
+ import importlib
186
+ module_name, class_name = value['_class'].rsplit('.', 1)
187
+ module = importlib.import_module(module_name)
188
+ obj_class = getattr(module, class_name)
189
+ # Remove marker fields before passing to _from_dict
190
+ obj_data = {k: v for k, v in value.items() if k not in ('__object__', '_class')}
191
+ return obj_class._from_dict(obj_data)
192
+ elif '__object_dict__' in value:
193
+ # Reconstruct dictionary with object keys or values
194
+ reconstructed_dict = {}
195
+ for item in value['items']:
196
+ key_obj = cls._deserialize_value(item['key'])
197
+ value_obj = cls._deserialize_value(item['value'])
198
+ reconstructed_dict[key_obj] = value_obj
199
+ return reconstructed_dict
200
+ else:
201
+ # Recursively deserialize regular dict values
202
+ return {k: cls._deserialize_value(v) for k, v in value.items()}
203
+ elif isinstance(value, list):
204
+ # Recursively deserialize list items
205
+ return [cls._deserialize_value(item) for item in value]
206
+ else:
207
+ return value
208
+
209
+ @classmethod
210
+ def _from_dict(cls: type[T], data: dict) -> T:
211
+ """
212
+ Reconstruct object from dictionary.
213
+ Override in subclasses for custom deserialization.
214
+
215
+ Parameters
216
+ ----------
217
+ data : dict
218
+ Dictionary representation to reconstruct from.
219
+
220
+ Returns
221
+ -------
222
+ T
223
+ Reconstructed object instance.
224
+ """
225
+ obj = cls.__new__(cls)
226
+ reconstructed = {}
227
+ for key, value in data.items():
228
+ reconstructed[key] = cls._deserialize_value(value)
229
+ obj.__dict__.update(reconstructed)
44
230
  return obj
231
+
232
+ @classmethod
233
+ def from_database(cls: type[T], name: str, table_name: str = None) -> T:
234
+ """
235
+ Pull object from the database by name.
236
+
237
+ Subclasses must define a '_table_name' class variable (str or list of str)
238
+ unless table_name is explicitly provided.
45
239
 
240
+ Parameters
241
+ ----------
242
+ name : str
243
+ Name of the object to retrieve.
244
+ table_name : str, optional
245
+ Specific table to search. If provided, '_table_name' is not required.
246
+ If None, uses class's _table_name.
46
247
 
248
+ Returns
249
+ -------
250
+ T
251
+ Instance of the class.
252
+
253
+ Raises
254
+ ------
255
+ NotImplementedError
256
+ If the subclass doesn't define '_table_name' and table_name is not provided.
257
+ ValueError
258
+ If the object name is not found in any of the tables.
259
+ """
260
+ from steer_core.Data.DataManager import DataManager
261
+
262
+ database = DataManager()
263
+
264
+ # Get list of tables to search
265
+ if table_name:
266
+ tables_to_search = [table_name]
267
+ else:
268
+ # Only check for _table_name if table_name wasn't provided
269
+ if not hasattr(cls, '_table_name'):
270
+ raise NotImplementedError(
271
+ f"{cls.__name__} must define a '_table_name' class variable "
272
+ "or provide 'table_name' argument"
273
+ )
274
+
275
+ if isinstance(cls._table_name, (list, tuple)):
276
+ tables_to_search = cls._table_name
277
+ else:
278
+ tables_to_search = [cls._table_name]
279
+
280
+ # Try each table until found
281
+ for table in tables_to_search:
282
+ available_materials = database.get_unique_values(table, "name")
283
+
284
+ if name in available_materials:
285
+ data = database.get_data(table, condition=f"name = '{name}'")
286
+ serialized_bytes = data["object"].iloc[0]
287
+ return cls.deserialize(serialized_bytes)
288
+
289
+ # Not found in any table
290
+ all_available = []
291
+ for table in tables_to_search:
292
+ all_available.extend(database.get_unique_values(table, "name"))
293
+
294
+ raise ValueError(
295
+ f"'{name}' not found in tables {tables_to_search}. "
296
+ f"Available: {all_available}"
297
+ )
298
+
@@ -208,8 +208,9 @@ class ValidationMixin:
208
208
  ValueError
209
209
  If the value is not a positive float.
210
210
  """
211
+ value = float(value) # Ensure value is float
211
212
  if not isinstance(value, (int, float)):
212
- raise ValueError(f"{name} must be a positive float. Provided: {value}.")
213
+ raise ValueError(f"{name} must be a positive float. Provided: {value} of type {type(value).__name__}.")
213
214
 
214
215
  @staticmethod
215
216
  def validate_positive_int(value: int, name: str) -> None:
steer_core/__init__.py CHANGED
@@ -1,7 +1,4 @@
1
- __version__ = "0.1.30"
2
-
3
- # datamanager import
4
- from .DataManager import DataManager
1
+ __version__ = "0.1.33"
5
2
 
6
3
  from .Mixins.Colors import ColorMixin
7
4
  from .Mixins.Coordinates import CoordinateMixin
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steer-core
3
- Version: 0.1.30
3
+ Version: 0.1.33
4
4
  Summary: Modelling energy storage from cell to site - STEER OpenCell Design
5
5
  Author-email: Nicholas Siemons <nsiemons@stanford.edu>
6
6
  Maintainer-email: Nicholas Siemons <nsiemons@stanford.edu>
@@ -1,12 +1,11 @@
1
- steer_core/DataManager.py,sha256=XlURPvXYS05jd6bj7M4QkNU9Dwp3s1X3xjy3AIxX6No,12256
2
- steer_core/__init__.py,sha256=4vqKn3rLX5TKwgoOHGdqZusR6xbPr_3zyPxh6zK1GFg,380
1
+ steer_core/__init__.py,sha256=uaSr9hFWvTFJQhMD0bP2Rvg3e7RcybXAGZGBGaWY4MA,321
3
2
  steer_core/Constants/Units.py,sha256=QIV_lDX7rANH-MKP90jOyiXbGueL15LILKMNr5dSWoI,714
4
3
  steer_core/Constants/Universal.py,sha256=5FWdrex5NiI2DResDmwO7GIvGN2B0DNtdlG1l-ysDh8,41
5
4
  steer_core/Constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
5
  steer_core/ContextManagers/ContextManagers.py,sha256=4rSeBdBi6xtKLMAbERklrYmZlbFr0zceAiwu-gjTR38,1869
7
6
  steer_core/ContextManagers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ steer_core/Data/DataManager.py,sha256=AUbuK1lmibTeY5oZ_RSmfof9FyFd9HQ5J2vMoLWsuAo,14707
8
8
  steer_core/Data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- steer_core/Data/database.db,sha256=hQ6rHo_htf-xsq5_saleJlQGjc1nJAS6tCwAjStjyMY,1716224
10
9
  steer_core/Decorators/Coordinates.py,sha256=MxUWXQNrR9Q0_p4gGAywS4qnPAztajJzSay1Cu6lCRQ,1441
11
10
  steer_core/Decorators/General.py,sha256=lc7YdvxU-JDo8b4kunVzSjxcB3_8C185458HrXQq-lk,970
12
11
  steer_core/Decorators/Objects.py,sha256=aYaRQBFgdSE0IB4QgBVfb6GhEPagoU6TRNrW_pOaqQI,506
@@ -14,12 +13,12 @@ steer_core/Decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
14
13
  steer_core/Mixins/Colors.py,sha256=vbo44Fr0oeziwHJ-tu7ojG-GzqFc2LBcT_hH4szvPFc,6796
15
14
  steer_core/Mixins/Coordinates.py,sha256=DEPKvySoiDT8JhQSEQDiQgAxGjEJK7MezuZysSvf-UU,36259
16
15
  steer_core/Mixins/Data.py,sha256=c313F85muxlBHQ6yl6AKrifNyV2toHvVwEy35fNNUNE,4434
17
- steer_core/Mixins/Dunder.py,sha256=591oDGiRPdLH1bDIc1FUw33eeRtSc4pC7UbKEIGPm1I,7035
18
- steer_core/Mixins/Plotter.py,sha256=2W8PQok3AHogS8rgpOkaC5UiJGXu9ZxXSrKW5lOQp5w,13503
19
- steer_core/Mixins/Serializer.py,sha256=20ZOP7cKWwD-JH-XwBMN-qOoxFohQMn4XBr52U6cwTE,1023
20
- steer_core/Mixins/TypeChecker.py,sha256=dzTu6q6xfz0Op3Yhu7vA-3-8D02iHZPBeAdX9ng9-WM,10454
16
+ steer_core/Mixins/Dunder.py,sha256=cIwh1VhcwzlwelUo2eM1KllVtxZddrkr6g0a5CkXea8,7146
17
+ steer_core/Mixins/Plotter.py,sha256=wRRF0C5fz_6polCKKRVnZ07UFL4HBohZ5K2BSm_ULsg,13505
18
+ steer_core/Mixins/Serializer.py,sha256=a3-J1ESM0UkO7b8Ctzjj605-p9bfzoFjJydM8k2grEQ,10458
19
+ steer_core/Mixins/TypeChecker.py,sha256=eXgu1G4d2_btNg5AJ3UkTXpoFJ79bc6tSasInCnXqc0,10539
21
20
  steer_core/Mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- steer_core-0.1.30.dist-info/METADATA,sha256=fhJvQG2VK24GIL6uCgShSNtqJVVFk9dMfor16Zsj1nk,1601
23
- steer_core-0.1.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- steer_core-0.1.30.dist-info/top_level.txt,sha256=6LFpGCSDE_SqRoT7raeM3Ax7KTBKQnyXLXxM9kXtw5M,11
25
- steer_core-0.1.30.dist-info/RECORD,,
21
+ steer_core-0.1.33.dist-info/METADATA,sha256=Cgqe7hQ4CZxG1bSCAZhO1-CwDh5aJjsXgoOMBaZGUY8,1601
22
+ steer_core-0.1.33.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ steer_core-0.1.33.dist-info/top_level.txt,sha256=6LFpGCSDE_SqRoT7raeM3Ax7KTBKQnyXLXxM9kXtw5M,11
24
+ steer_core-0.1.33.dist-info/RECORD,,
Binary file