activemodel 0.8.0__py3-none-any.whl → 0.9.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.
- activemodel/base_model.py +79 -6
- activemodel/mixins/pydantic_json.py +10 -5
- activemodel/pytest/transaction.py +1 -1
- activemodel/types/typeid.py +0 -2
- {activemodel-0.8.0.dist-info → activemodel-0.9.0.dist-info}/METADATA +1 -1
- {activemodel-0.8.0.dist-info → activemodel-0.9.0.dist-info}/RECORD +9 -9
- {activemodel-0.8.0.dist-info → activemodel-0.9.0.dist-info}/WHEEL +0 -0
- {activemodel-0.8.0.dist-info → activemodel-0.9.0.dist-info}/entry_points.txt +0 -0
- {activemodel-0.8.0.dist-info → activemodel-0.9.0.dist-info}/licenses/LICENSE +0 -0
activemodel/base_model.py
CHANGED
|
@@ -4,12 +4,13 @@ from uuid import UUID
|
|
|
4
4
|
|
|
5
5
|
import pydash
|
|
6
6
|
import sqlalchemy as sa
|
|
7
|
+
from sqlalchemy.orm.attributes import flag_modified as sa_flag_modified
|
|
8
|
+
from sqlalchemy.orm.base import instance_state
|
|
7
9
|
import sqlmodel as sm
|
|
8
10
|
from sqlalchemy import Connection, event
|
|
9
11
|
from sqlalchemy.orm import Mapper, declared_attr
|
|
10
|
-
from sqlmodel import
|
|
12
|
+
from sqlmodel import Column, Field, Session, SQLModel, inspect, select
|
|
11
13
|
from typeid import TypeID
|
|
12
|
-
from inspect import isclass
|
|
13
14
|
|
|
14
15
|
from activemodel.mixins.pydantic_json import PydanticJSONMixin
|
|
15
16
|
|
|
@@ -214,7 +215,26 @@ class BaseModel(SQLModel):
|
|
|
214
215
|
|
|
215
216
|
return self
|
|
216
217
|
|
|
218
|
+
def refresh(self):
|
|
219
|
+
"Refreshes an object from the database"
|
|
220
|
+
|
|
221
|
+
with get_session() as session:
|
|
222
|
+
if (
|
|
223
|
+
old_session := Session.object_session(self)
|
|
224
|
+
) and old_session is not session:
|
|
225
|
+
old_session.expunge(self)
|
|
226
|
+
|
|
227
|
+
session.add(self)
|
|
228
|
+
session.refresh(self)
|
|
229
|
+
|
|
230
|
+
# Only call the transform method if the class is a subclass of PydanticJSONMixin
|
|
231
|
+
if issubclass(self.__class__, PydanticJSONMixin):
|
|
232
|
+
self.__class__.__transform_dict_to_pydantic__(self)
|
|
233
|
+
|
|
234
|
+
return self
|
|
235
|
+
|
|
217
236
|
# TODO shouldn't this be handled by pydantic?
|
|
237
|
+
# TODO where is this actually used? shoudl prob remove this
|
|
218
238
|
def json(self, **kwargs):
|
|
219
239
|
return json.dumps(self.dict(), default=str, **kwargs)
|
|
220
240
|
|
|
@@ -236,6 +256,29 @@ class BaseModel(SQLModel):
|
|
|
236
256
|
def is_new(self) -> bool:
|
|
237
257
|
return not self._sa_instance_state.has_identity
|
|
238
258
|
|
|
259
|
+
def flag_modified(self, *args: str):
|
|
260
|
+
"""
|
|
261
|
+
Flag one or more fields as modified. Useful for marking a field containing sub-objects as modified.
|
|
262
|
+
|
|
263
|
+
Will throw an error if an invalid field is passed.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
assert len(args) > 0, "Must pass at least one field name"
|
|
267
|
+
|
|
268
|
+
for field_name in args:
|
|
269
|
+
if field_name not in self.__fields__:
|
|
270
|
+
raise ValueError(f"Field '{field_name}' does not exist in the model.")
|
|
271
|
+
|
|
272
|
+
# check if the field exists
|
|
273
|
+
sa_flag_modified(self, field_name)
|
|
274
|
+
|
|
275
|
+
def modified_fields(self) -> set[str]:
|
|
276
|
+
"set of fields that are modified"
|
|
277
|
+
|
|
278
|
+
insp = inspect(self)
|
|
279
|
+
|
|
280
|
+
return {attr.key for attr in insp.attrs if attr.history.has_changes()}
|
|
281
|
+
|
|
239
282
|
@classmethod
|
|
240
283
|
def find_or_create_by(cls, **kwargs):
|
|
241
284
|
"""
|
|
@@ -268,13 +311,15 @@ class BaseModel(SQLModel):
|
|
|
268
311
|
return new_model
|
|
269
312
|
|
|
270
313
|
@classmethod
|
|
271
|
-
def
|
|
314
|
+
def primary_key_column(cls) -> Column:
|
|
272
315
|
"""
|
|
273
316
|
Returns the primary key column of the model by inspecting SQLAlchemy field information.
|
|
274
317
|
|
|
275
318
|
>>> ExampleModel.primary_key_field().name
|
|
276
319
|
"""
|
|
320
|
+
|
|
277
321
|
# TODO note_schema.__class__.__table__.primary_key
|
|
322
|
+
# TODO no reason why this couldn't be cached
|
|
278
323
|
|
|
279
324
|
pk_columns = list(cls.__table__.primary_key.columns)
|
|
280
325
|
|
|
@@ -297,9 +342,8 @@ class BaseModel(SQLModel):
|
|
|
297
342
|
@classmethod
|
|
298
343
|
def get(cls, *args: t.Any, **kwargs: t.Any):
|
|
299
344
|
"""
|
|
300
|
-
Gets a single record from the database. Pass an PK ID or
|
|
345
|
+
Gets a single record (or None) from the database. Pass an PK ID or kwargs to filter by.
|
|
301
346
|
"""
|
|
302
|
-
|
|
303
347
|
# TODO id is hardcoded, not good! Need to dynamically pick the best uid field
|
|
304
348
|
id_field_name = "id"
|
|
305
349
|
|
|
@@ -313,8 +357,37 @@ class BaseModel(SQLModel):
|
|
|
313
357
|
with get_session() as session:
|
|
314
358
|
return session.exec(statement).first()
|
|
315
359
|
|
|
360
|
+
@classmethod
|
|
361
|
+
def one(cls, *args: t.Any, **kwargs: t.Any):
|
|
362
|
+
"""
|
|
363
|
+
Gets a single record from the database. Pass an PK ID or a kwarg to filter by.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
args, kwargs = cls.__process_filter_args__(*args, **kwargs)
|
|
367
|
+
statement = select(cls).filter(*args).filter_by(**kwargs)
|
|
368
|
+
|
|
369
|
+
with get_session() as session:
|
|
370
|
+
return session.exec(statement).one()
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
def __process_filter_args__(cls, *args: t.Any, **kwargs: t.Any):
|
|
374
|
+
"""
|
|
375
|
+
Helper method to process filter arguments and implement some nice DX for our devs.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
id_field_name = cls.primary_key_column().name
|
|
379
|
+
|
|
380
|
+
# special case for getting by ID without having to specify the field name
|
|
381
|
+
# TODO should dynamically add new pk types based on column definition
|
|
382
|
+
if len(args) == 1 and isinstance(args[0], (int, TypeID, str, UUID)):
|
|
383
|
+
kwargs[id_field_name] = args[0]
|
|
384
|
+
args = ()
|
|
385
|
+
|
|
386
|
+
return args, kwargs
|
|
387
|
+
|
|
316
388
|
@classmethod
|
|
317
389
|
def all(cls):
|
|
390
|
+
"get a generator for all records in the database"
|
|
318
391
|
with get_session() as session:
|
|
319
392
|
results = session.exec(sm.select(cls))
|
|
320
393
|
|
|
@@ -325,7 +398,7 @@ class BaseModel(SQLModel):
|
|
|
325
398
|
@classmethod
|
|
326
399
|
def sample(cls):
|
|
327
400
|
"""
|
|
328
|
-
Pick a random record from the database.
|
|
401
|
+
Pick a random record from the database. Raises if none exist.
|
|
329
402
|
|
|
330
403
|
Helpful for testing and console debugging.
|
|
331
404
|
"""
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Need to store nested Pydantic models in PostgreSQL using FastAPI and SQLModel.
|
|
3
|
+
|
|
4
|
+
SQLModel lacks a direct JSONField equivalent (like Tortoise ORM's JSONField), making it tricky to handle nested model data as JSON in the DB.
|
|
5
|
+
|
|
6
|
+
Extensive discussion on the problem: https://github.com/fastapi/sqlmodel/issues/63
|
|
3
7
|
"""
|
|
4
8
|
|
|
5
9
|
from types import UnionType
|
|
6
10
|
from typing import get_args, get_origin
|
|
7
11
|
|
|
8
12
|
from pydantic import BaseModel as PydanticBaseModel
|
|
9
|
-
from sqlalchemy.orm import reconstructor
|
|
13
|
+
from sqlalchemy.orm import reconstructor, attributes
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
class PydanticJSONMixin:
|
|
@@ -26,6 +30,8 @@ class PydanticJSONMixin:
|
|
|
26
30
|
|
|
27
31
|
- Reconstructor only runs once, when the object is loaded.
|
|
28
32
|
- We manually call this method on save(), etc to ensure the pydantic types are maintained
|
|
33
|
+
- `set_committed_value` sets Pydantic models as committed, avoiding `setattr` marking fields as modified
|
|
34
|
+
after loading from the database.
|
|
29
35
|
"""
|
|
30
36
|
# TODO do we need to inspect sa_type
|
|
31
37
|
for field_name, field_info in self.model_fields.items():
|
|
@@ -73,10 +79,9 @@ class PydanticJSONMixin:
|
|
|
73
79
|
model_cls, PydanticBaseModel
|
|
74
80
|
):
|
|
75
81
|
parsed_value = [model_cls(**item) for item in raw_value]
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
attributes.set_committed_value(self, field_name, parsed_value)
|
|
78
83
|
continue
|
|
79
84
|
|
|
80
85
|
# single class
|
|
81
86
|
if issubclass(model_cls, PydanticBaseModel):
|
|
82
|
-
|
|
87
|
+
attributes.set_committed_value(self, field_name, model_cls(**raw_value))
|
|
@@ -28,7 +28,7 @@ def database_reset_transaction():
|
|
|
28
28
|
|
|
29
29
|
engine = SessionManager.get_instance().get_engine()
|
|
30
30
|
|
|
31
|
-
logger.info("starting database transaction")
|
|
31
|
+
logger.info("starting global database transaction")
|
|
32
32
|
|
|
33
33
|
with engine.begin() as connection:
|
|
34
34
|
transaction = connection.begin_nested()
|
activemodel/types/typeid.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
activemodel/__init__.py,sha256=q_lHQyIM70ApvjduTo9GtenQjJXsfYZsAAquD_51kF4,137
|
|
2
|
-
activemodel/base_model.py,sha256=
|
|
2
|
+
activemodel/base_model.py,sha256=Xn5RCN-tAmGGkhaH8gnT2PFIkbefeSv85bPP41cJp14,14715
|
|
3
3
|
activemodel/celery.py,sha256=L1vKcO_HoPA5ZCfsXjxgPpDUMYDuoQMakGA9rppN7Lo,897
|
|
4
4
|
activemodel/errors.py,sha256=wycWYmk9ws4TZpxvTdtXVy2SFESb8NqKgzdivBoF0vw,115
|
|
5
5
|
activemodel/get_column_from_field_patch.py,sha256=wAEDm_ZvSqyJwfgkXVpxsevw11hd-7VLy7zuJG8Ak7Y,4986
|
|
@@ -8,17 +8,17 @@ activemodel/query_wrapper.py,sha256=rNdvueppMse2MIi-RafTEC34GPGRal_wqH2CzhmlWS8,
|
|
|
8
8
|
activemodel/session_manager.py,sha256=Vtg8Lf8vUNPegdRW-fyE-Ng5wtN3hTMfUezdFUiJ1fs,4585
|
|
9
9
|
activemodel/utils.py,sha256=g17UqkphzTmb6YdpmYwT1TM00eDiXXuWn39-xNiu0AA,2112
|
|
10
10
|
activemodel/mixins/__init__.py,sha256=05EQl2u_Wgf_wkly-GTaTsR7zWpmpKcb96Js7r_rZTw,160
|
|
11
|
-
activemodel/mixins/pydantic_json.py,sha256=
|
|
11
|
+
activemodel/mixins/pydantic_json.py,sha256=0pprGZA95BGZL4WOh--NJcvxLWey4YW85lLk4GGTjFM,3530
|
|
12
12
|
activemodel/mixins/soft_delete.py,sha256=Ax4mGsQI7AVTE8c4GiWxpyB_W179-dDct79GtjP0owU,461
|
|
13
13
|
activemodel/mixins/timestamps.py,sha256=Q-IFljeVVJQqw3XHdOi7dkqzefiVg1zhJvq_bldpmjg,992
|
|
14
14
|
activemodel/mixins/typeid.py,sha256=DGjlIg8PRBYoaBbWkkxc6jkScyl-p53KuSR98lLgAvE,1284
|
|
15
15
|
activemodel/pytest/__init__.py,sha256=W9KKQHbPkyq0jrMXaiL8hG2Nsbjy_LN9HhvgGm8W_7g,98
|
|
16
|
-
activemodel/pytest/transaction.py,sha256=
|
|
16
|
+
activemodel/pytest/transaction.py,sha256=ln-3N5tXHT0fqy6a8m_NIYg5AXAeA2hDuftQtFxNqi4,2600
|
|
17
17
|
activemodel/pytest/truncate.py,sha256=IGiPLkUm2yyOKww6c6CKcVbwi2xAAFBopx9q2ABfu8w,1582
|
|
18
18
|
activemodel/types/__init__.py,sha256=y5fiGVtPJxGEhuf-TvyrkhM2yaKRcIWo6XAx-CFFjM8,31
|
|
19
|
-
activemodel/types/typeid.py,sha256=
|
|
20
|
-
activemodel-0.
|
|
21
|
-
activemodel-0.
|
|
22
|
-
activemodel-0.
|
|
23
|
-
activemodel-0.
|
|
24
|
-
activemodel-0.
|
|
19
|
+
activemodel/types/typeid.py,sha256=1xB79DGIC5-P-PcLpeZW9Ed_WjFOmmVW1yl2Q3pPJis,7250
|
|
20
|
+
activemodel-0.9.0.dist-info/METADATA,sha256=AWt9ERLLgE1X1aQvJSuPwDTBmqoH9EW2_zglUlJVfPY,9651
|
|
21
|
+
activemodel-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
22
|
+
activemodel-0.9.0.dist-info/entry_points.txt,sha256=YLX62TP_hR-n3HMBkdBex4W7XRiyOtIPkwy22puIjjQ,61
|
|
23
|
+
activemodel-0.9.0.dist-info/licenses/LICENSE,sha256=L8mmpX47rB-xtJ_HsK0zpfO6viEjxbLYGn70BMp8os4,1071
|
|
24
|
+
activemodel-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|