activemodel 0.12.0__py3-none-any.whl → 0.14.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 +92 -82
- activemodel/mixins/pydantic_json.py +27 -7
- activemodel/query_wrapper.py +57 -1
- activemodel/session_manager.py +8 -0
- {activemodel-0.12.0.dist-info → activemodel-0.14.0.dist-info}/METADATA +3 -2
- {activemodel-0.12.0.dist-info → activemodel-0.14.0.dist-info}/RECORD +9 -9
- {activemodel-0.12.0.dist-info → activemodel-0.14.0.dist-info}/WHEEL +0 -0
- {activemodel-0.12.0.dist-info → activemodel-0.14.0.dist-info}/entry_points.txt +0 -0
- {activemodel-0.12.0.dist-info → activemodel-0.14.0.dist-info}/licenses/LICENSE +0 -0
activemodel/base_model.py
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import typing as t
|
|
3
|
+
import textcase
|
|
3
4
|
from uuid import UUID
|
|
5
|
+
from contextlib import nullcontext
|
|
4
6
|
|
|
5
7
|
import sqlalchemy as sa
|
|
6
8
|
import sqlmodel as sm
|
|
7
|
-
import textcase
|
|
8
|
-
from sqlalchemy import Connection, event
|
|
9
9
|
from sqlalchemy.dialects.postgresql import insert as postgres_insert
|
|
10
|
-
from sqlalchemy.orm import Mapper, declared_attr
|
|
11
10
|
from sqlalchemy.orm.attributes import flag_modified as sa_flag_modified
|
|
12
|
-
from sqlalchemy.orm.base import instance_state
|
|
13
11
|
from sqlmodel import Column, Field, Session, SQLModel, inspect, select
|
|
14
12
|
from typeid import TypeID
|
|
13
|
+
from sqlalchemy.orm import declared_attr
|
|
15
14
|
|
|
16
15
|
from activemodel.mixins.pydantic_json import PydanticJSONMixin
|
|
17
16
|
|
|
18
17
|
# NOTE: this patches a core method in sqlmodel to support db comments
|
|
19
18
|
from . import get_column_from_field_patch # noqa: F401
|
|
20
|
-
from .logger import logger
|
|
21
19
|
from .query_wrapper import QueryWrapper
|
|
22
20
|
from .session_manager import get_session
|
|
23
21
|
|
|
@@ -42,85 +40,46 @@ SQLModel.metadata.naming_convention = POSTGRES_INDEXES_NAMING_CONVENTION
|
|
|
42
40
|
|
|
43
41
|
class BaseModel(SQLModel):
|
|
44
42
|
"""
|
|
45
|
-
Base model class to inherit from so we can hate python less
|
|
43
|
+
Base model class to inherit from so we can hate python less.
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
Some notes:
|
|
48
46
|
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
47
|
+
- Inspired by https://github.com/woofz/sqlmodel-basecrud/blob/main/sqlmodel_basecrud/basecrud.py
|
|
48
|
+
- lifecycle hooks are modeled after Rails.
|
|
49
|
+
- class docstrings are converted to table-level comments
|
|
50
|
+
- save(), delete(), select(), where(), and other easy methods you would expect in a real ORM
|
|
52
51
|
- Fixes foreign key naming conventions
|
|
52
|
+
- Sane table names
|
|
53
|
+
|
|
54
|
+
Here's how hooks work:
|
|
55
|
+
|
|
56
|
+
Create/Update: before_create, after_create, before_update, after_update, before_save, after_save, around_save
|
|
57
|
+
Delete: before_delete, after_delete, around_delete
|
|
58
|
+
|
|
59
|
+
around_* hooks must be context managers (method returning a CM or a CM attribute).
|
|
60
|
+
Ordering (create): before_create -> before_save -> (enter around_save) -> persist -> after_create -> after_save -> (exit around_save)
|
|
61
|
+
Ordering (update): before_update -> before_save -> (enter around_save) -> persist -> after_update -> after_save -> (exit around_save)
|
|
62
|
+
Delete: before_delete -> (enter around_delete) -> delete -> after_delete -> (exit around_delete)
|
|
63
|
+
|
|
64
|
+
# TODO document this in activemodel, this is an interesting edge case
|
|
65
|
+
# https://claude.ai/share/f09e4f70-2ff7-4cd0-abff-44645134693a
|
|
66
|
+
|
|
53
67
|
"""
|
|
54
68
|
|
|
55
|
-
# this is used for table-level comments
|
|
56
69
|
__table_args__ = None
|
|
57
70
|
|
|
58
71
|
@classmethod
|
|
59
72
|
def __init_subclass__(cls, **kwargs):
|
|
60
|
-
"Setup automatic sqlalchemy lifecycle events for the class"
|
|
61
|
-
|
|
62
73
|
super().__init_subclass__(**kwargs)
|
|
63
74
|
|
|
64
75
|
from sqlmodel._compat import set_config_value
|
|
65
76
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
77
|
+
# Enables field-level docstrings on the pydantic `description` field, which we
|
|
78
|
+
# copy into table/column comments by patching SQLModel internals elsewhere.
|
|
68
79
|
set_config_value(model=cls, parameter="use_attribute_docstrings", value=True)
|
|
69
80
|
|
|
70
81
|
cls._apply_class_doc()
|
|
71
82
|
|
|
72
|
-
def event_wrapper(method_name: str):
|
|
73
|
-
"""
|
|
74
|
-
This does smart heavy lifting for us to make sqlalchemy lifecycle events nicer to work with:
|
|
75
|
-
|
|
76
|
-
* Passes the target first to the lifecycle method, so it feels like an instance method
|
|
77
|
-
* Allows as little as a single positional argument, so methods can be simple
|
|
78
|
-
* Removes the need for decorators or anything fancy on the subclass
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
def wrapper(mapper: Mapper, connection: Connection, target: BaseModel):
|
|
82
|
-
if hasattr(cls, method_name):
|
|
83
|
-
method = getattr(cls, method_name)
|
|
84
|
-
|
|
85
|
-
if callable(method):
|
|
86
|
-
arg_count = method.__code__.co_argcount
|
|
87
|
-
|
|
88
|
-
if arg_count == 1: # Just self/cls
|
|
89
|
-
method(target)
|
|
90
|
-
elif arg_count == 2: # Self, mapper
|
|
91
|
-
method(target, mapper)
|
|
92
|
-
elif arg_count == 3: # Full signature
|
|
93
|
-
method(target, mapper, connection)
|
|
94
|
-
else:
|
|
95
|
-
raise TypeError(
|
|
96
|
-
f"Method {method_name} must accept either 1 to 3 arguments, got {arg_count}"
|
|
97
|
-
)
|
|
98
|
-
else:
|
|
99
|
-
logger.warning(
|
|
100
|
-
"SQLModel lifecycle hook found, but not callable hook_name=%s",
|
|
101
|
-
method_name,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
return wrapper
|
|
105
|
-
|
|
106
|
-
event.listen(cls, "before_insert", event_wrapper("before_insert"))
|
|
107
|
-
event.listen(cls, "before_update", event_wrapper("before_update"))
|
|
108
|
-
|
|
109
|
-
# before_save maps to two type of events
|
|
110
|
-
event.listen(cls, "before_insert", event_wrapper("before_save"))
|
|
111
|
-
event.listen(cls, "before_update", event_wrapper("before_save"))
|
|
112
|
-
|
|
113
|
-
# now, let's handle after_* variants
|
|
114
|
-
event.listen(cls, "after_insert", event_wrapper("after_insert"))
|
|
115
|
-
event.listen(cls, "after_update", event_wrapper("after_update"))
|
|
116
|
-
|
|
117
|
-
# after_save maps to two type of events
|
|
118
|
-
event.listen(cls, "after_insert", event_wrapper("after_save"))
|
|
119
|
-
event.listen(cls, "after_update", event_wrapper("after_save"))
|
|
120
|
-
|
|
121
|
-
# def foreign_key()
|
|
122
|
-
# table.id
|
|
123
|
-
|
|
124
83
|
@classmethod
|
|
125
84
|
def _apply_class_doc(cls):
|
|
126
85
|
"""
|
|
@@ -234,36 +193,81 @@ class BaseModel(SQLModel):
|
|
|
234
193
|
return result
|
|
235
194
|
|
|
236
195
|
def delete(self):
|
|
237
|
-
"Delete
|
|
196
|
+
"""Delete instance running delete hooks and optional around_delete context manager."""
|
|
197
|
+
|
|
198
|
+
cm = self._get_around_context_manager("around_delete") or nullcontext()
|
|
238
199
|
|
|
239
200
|
with get_session() as session:
|
|
240
|
-
if
|
|
201
|
+
if (
|
|
202
|
+
old_session := Session.object_session(self)
|
|
203
|
+
) and old_session is not session:
|
|
241
204
|
old_session.expunge(self)
|
|
242
|
-
|
|
243
205
|
session.delete(self)
|
|
244
|
-
|
|
245
|
-
|
|
206
|
+
|
|
207
|
+
self._call_hook("before_delete")
|
|
208
|
+
with cm:
|
|
209
|
+
session.commit()
|
|
210
|
+
self._call_hook("after_delete")
|
|
211
|
+
|
|
212
|
+
return True
|
|
246
213
|
|
|
247
214
|
def save(self):
|
|
215
|
+
"""Persist instance running create/update hooks and optional around_save context manager."""
|
|
216
|
+
|
|
217
|
+
is_new = self.is_new()
|
|
218
|
+
cm = self._get_around_context_manager("around_save") or nullcontext()
|
|
219
|
+
|
|
248
220
|
with get_session() as session:
|
|
249
|
-
if
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
# to get around this, you need to remove it from the old one,
|
|
253
|
-
# then add it to the new one (below)
|
|
221
|
+
if (
|
|
222
|
+
old_session := Session.object_session(self)
|
|
223
|
+
) and old_session is not session:
|
|
254
224
|
old_session.expunge(self)
|
|
255
225
|
|
|
256
226
|
session.add(self)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
session
|
|
227
|
+
|
|
228
|
+
# the order and placement of these hooks is really important
|
|
229
|
+
# we need the current object to be in a session otherwise it will not be able to
|
|
230
|
+
# load any relationships.
|
|
231
|
+
self._call_hook("before_create" if is_new else "before_update")
|
|
232
|
+
self._call_hook("before_save")
|
|
233
|
+
|
|
234
|
+
with cm:
|
|
235
|
+
session.commit()
|
|
236
|
+
session.refresh(self)
|
|
237
|
+
|
|
238
|
+
self._call_hook("after_create" if is_new else "after_update")
|
|
239
|
+
self._call_hook("after_save")
|
|
260
240
|
|
|
261
241
|
# Only call the transform method if the class is a subclass of PydanticJSONMixin
|
|
262
242
|
if issubclass(self.__class__, PydanticJSONMixin):
|
|
263
243
|
self.__class__.__transform_dict_to_pydantic__(self)
|
|
264
|
-
|
|
265
244
|
return self
|
|
266
245
|
|
|
246
|
+
def _call_hook(self, hook_name: str) -> None:
|
|
247
|
+
method = getattr(self, hook_name, None)
|
|
248
|
+
if callable(method):
|
|
249
|
+
if method.__code__.co_argcount != 1:
|
|
250
|
+
raise TypeError(
|
|
251
|
+
f"Hook '{hook_name}' must accept exactly 1 positional argument (self)"
|
|
252
|
+
)
|
|
253
|
+
method()
|
|
254
|
+
|
|
255
|
+
def _get_around_context_manager(self, name: str) -> t.ContextManager | None:
|
|
256
|
+
obj = getattr(self, name, None)
|
|
257
|
+
if obj is None:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
# If it's a callable (method/function), call it to obtain the CM
|
|
261
|
+
if callable(obj):
|
|
262
|
+
obj = obj()
|
|
263
|
+
|
|
264
|
+
cm = obj
|
|
265
|
+
if not (hasattr(cm, "__enter__") and hasattr(cm, "__exit__")):
|
|
266
|
+
raise TypeError(
|
|
267
|
+
f"{name} must return or be a context manager implementing __enter__/__exit__"
|
|
268
|
+
)
|
|
269
|
+
return t.cast(t.ContextManager, cm)
|
|
270
|
+
|
|
267
271
|
def refresh(self):
|
|
268
272
|
"Refreshes an object from the database"
|
|
269
273
|
|
|
@@ -284,6 +288,7 @@ class BaseModel(SQLModel):
|
|
|
284
288
|
|
|
285
289
|
# TODO shouldn't this be handled by pydantic?
|
|
286
290
|
# TODO where is this actually used? shoudl prob remove this
|
|
291
|
+
# TODO should we even do this? Can we specify a better json rendering class?
|
|
287
292
|
def json(self, **kwargs):
|
|
288
293
|
return json.dumps(self.dict(), default=str, **kwargs)
|
|
289
294
|
|
|
@@ -299,7 +304,12 @@ class BaseModel(SQLModel):
|
|
|
299
304
|
# TODO got to be a better way to fwd these along...
|
|
300
305
|
@classmethod
|
|
301
306
|
def first(cls):
|
|
302
|
-
|
|
307
|
+
# TODO should use dynamic pk
|
|
308
|
+
return cls.select().order_by(sa.desc(cls.id)).first()
|
|
309
|
+
|
|
310
|
+
# @classmethod
|
|
311
|
+
# def last(cls):
|
|
312
|
+
# return cls.select().first()
|
|
303
313
|
|
|
304
314
|
# TODO throw an error if this field is set on the model
|
|
305
315
|
def is_new(self) -> bool:
|
|
@@ -6,9 +6,9 @@ SQLModel lacks a direct JSONField equivalent (like Tortoise ORM's JSONField), ma
|
|
|
6
6
|
Extensive discussion on the problem: https://github.com/fastapi/sqlmodel/issues/63
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from types import UnionType
|
|
10
9
|
from typing import get_args, get_origin
|
|
11
|
-
|
|
10
|
+
import typing
|
|
11
|
+
import types
|
|
12
12
|
from pydantic import BaseModel as PydanticBaseModel
|
|
13
13
|
from sqlalchemy.orm import reconstructor, attributes
|
|
14
14
|
|
|
@@ -21,6 +21,11 @@ class PydanticJSONMixin:
|
|
|
21
21
|
|
|
22
22
|
>>> class ExampleWithJSON(BaseModel, PydanticJSONMixin, table=True):
|
|
23
23
|
>>> list_field: list[SubObject] = Field(sa_type=JSONB()
|
|
24
|
+
|
|
25
|
+
Notes:
|
|
26
|
+
|
|
27
|
+
- Tuples of pydantic models are not supported, only lists.
|
|
28
|
+
- Nested lists of pydantic models are not supported, e.g. list[list[SubObject]]
|
|
24
29
|
"""
|
|
25
30
|
|
|
26
31
|
@reconstructor
|
|
@@ -37,6 +42,7 @@ class PydanticJSONMixin:
|
|
|
37
42
|
for field_name, field_info in self.model_fields.items():
|
|
38
43
|
raw_value = getattr(self, field_name, None)
|
|
39
44
|
|
|
45
|
+
# if the field is not set on the model, we can avoid doing anything with it
|
|
40
46
|
if raw_value is None:
|
|
41
47
|
continue
|
|
42
48
|
|
|
@@ -44,32 +50,43 @@ class PydanticJSONMixin:
|
|
|
44
50
|
origin = get_origin(annotation)
|
|
45
51
|
|
|
46
52
|
# e.g. `dict` or `dict[str, str]`, we don't want to do anything with these
|
|
47
|
-
if origin
|
|
53
|
+
if origin in (dict, tuple):
|
|
48
54
|
continue
|
|
49
55
|
|
|
50
56
|
annotation_args = get_args(annotation)
|
|
51
57
|
is_top_level_list = origin is list
|
|
58
|
+
model_cls = annotation
|
|
52
59
|
|
|
60
|
+
# TODO not sure what was going on here...
|
|
53
61
|
# if origin is not None:
|
|
54
62
|
# assert annotation.__class__ == origin
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
# UnionType is only one way of defining an optional. If older typing syntax is used `Tuple[str] | None` the
|
|
65
|
+
# type annotation is different: `typing.Optional[typing.Tuple[float, float]]`. This is why we check both
|
|
66
|
+
# types below.
|
|
57
67
|
|
|
58
68
|
# e.g. SomePydanticModel | None or list[SomePydanticModel] | None
|
|
59
|
-
# annotation_args are (type, NoneType) in this case
|
|
60
|
-
if
|
|
69
|
+
# annotation_args are (type, NoneType) in this case. Remove NoneType.
|
|
70
|
+
if origin in (typing.Union, types.UnionType):
|
|
61
71
|
non_none_types = [t for t in annotation_args if t is not type(None)]
|
|
62
72
|
|
|
63
73
|
if len(non_none_types) == 1:
|
|
64
74
|
model_cls = non_none_types[0]
|
|
75
|
+
else:
|
|
76
|
+
# if there's more than one non-none type, it isn't meant to be serialized to JSON
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
model_cls_origin = get_origin(model_cls)
|
|
65
80
|
|
|
66
81
|
# e.g. list[SomePydanticModel] | None, we have to unpack it
|
|
67
82
|
# model_cls will print as a list, but it contains a subtype if you dig into it
|
|
68
83
|
if (
|
|
69
|
-
|
|
84
|
+
model_cls_origin is list
|
|
70
85
|
and len(list_annotation_args := get_args(model_cls)) == 1
|
|
71
86
|
):
|
|
72
87
|
model_cls = list_annotation_args[0]
|
|
88
|
+
model_cls_origin = get_origin(model_cls)
|
|
89
|
+
|
|
73
90
|
is_top_level_list = True
|
|
74
91
|
|
|
75
92
|
# e.g. list[SomePydanticModel] or list[SomePydanticModel] | None
|
|
@@ -82,6 +99,9 @@ class PydanticJSONMixin:
|
|
|
82
99
|
attributes.set_committed_value(self, field_name, parsed_value)
|
|
83
100
|
continue
|
|
84
101
|
|
|
102
|
+
if model_cls_origin in (list, tuple):
|
|
103
|
+
continue
|
|
104
|
+
|
|
85
105
|
# single class
|
|
86
106
|
if issubclass(model_cls, PydanticBaseModel):
|
|
87
107
|
attributes.set_committed_value(self, field_name, model_cls(**raw_value))
|
activemodel/query_wrapper.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import sqlmodel as sm
|
|
2
2
|
from sqlmodel.sql.expression import SelectOfScalar
|
|
3
|
+
from typing import overload, Literal
|
|
3
4
|
|
|
4
5
|
from activemodel.types.sqlalchemy_protocol import SQLAlchemyQueryMethods
|
|
5
6
|
|
|
@@ -48,6 +49,8 @@ class QueryWrapper[T: sm.SQLModel](SQLAlchemyQueryMethods[T]):
|
|
|
48
49
|
with get_session() as session:
|
|
49
50
|
return session.scalar(sm.select(sm.func.count()).select_from(self.target))
|
|
50
51
|
|
|
52
|
+
# TODO typing is broken here
|
|
53
|
+
# TODO would be great to define a default return type if nothing is found
|
|
51
54
|
def scalar(self):
|
|
52
55
|
"""
|
|
53
56
|
>>>
|
|
@@ -63,6 +66,17 @@ class QueryWrapper[T: sm.SQLModel](SQLAlchemyQueryMethods[T]):
|
|
|
63
66
|
with get_session() as session:
|
|
64
67
|
return session.delete(self.target)
|
|
65
68
|
|
|
69
|
+
def exists(self) -> bool:
|
|
70
|
+
"""Return True if the current query yields at least one row.
|
|
71
|
+
|
|
72
|
+
Uses the SQLAlchemy exists() construct against a LIMIT 1 version of
|
|
73
|
+
the current target for efficiency. Keeps the original target intact.
|
|
74
|
+
"""
|
|
75
|
+
with get_session() as session:
|
|
76
|
+
exists_stmt = sm.select(sm.exists(self.target))
|
|
77
|
+
result = session.scalar(exists_stmt)
|
|
78
|
+
return bool(result)
|
|
79
|
+
|
|
66
80
|
def __getattr__(self, name):
|
|
67
81
|
"""
|
|
68
82
|
This implements the magic that forwards function calls to sqlalchemy.
|
|
@@ -79,7 +93,7 @@ class QueryWrapper[T: sm.SQLModel](SQLAlchemyQueryMethods[T]):
|
|
|
79
93
|
|
|
80
94
|
def wrapper(*args, **kwargs):
|
|
81
95
|
result = sqlalchemy_target(*args, **kwargs)
|
|
82
|
-
self.target = result
|
|
96
|
+
self.target = result # type: ignore[assignment]
|
|
83
97
|
return self
|
|
84
98
|
|
|
85
99
|
return wrapper
|
|
@@ -95,6 +109,48 @@ class QueryWrapper[T: sm.SQLModel](SQLAlchemyQueryMethods[T]):
|
|
|
95
109
|
|
|
96
110
|
return compile_sql(self.target)
|
|
97
111
|
|
|
112
|
+
@overload
|
|
113
|
+
def sample(self) -> T | None: ...
|
|
114
|
+
|
|
115
|
+
@overload
|
|
116
|
+
def sample(self, n: Literal[1]) -> T | None: ...
|
|
117
|
+
|
|
118
|
+
@overload
|
|
119
|
+
def sample(self, n: int) -> list[T]: ...
|
|
120
|
+
|
|
121
|
+
def sample(self, n: int = 1) -> T | None | list[T]:
|
|
122
|
+
"""Return a random sample of rows from the current query.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
n: int
|
|
127
|
+
Number of rows to return. Defaults to 1.
|
|
128
|
+
|
|
129
|
+
Behavior
|
|
130
|
+
--------
|
|
131
|
+
- Returns a single model instance when ``n == 1`` (or ``None`` if no rows)
|
|
132
|
+
- Returns a list[Model] when ``n > 1`` (possibly empty list when no rows)
|
|
133
|
+
- Sampling is performed by appending an ``ORDER BY RANDOM()`` / ``func.random()``
|
|
134
|
+
and ``LIMIT n`` clause to the existing query target.
|
|
135
|
+
- Keeps original query intact (does not mutate ``self.target``) so further
|
|
136
|
+
chaining works as expected.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
if n < 1:
|
|
140
|
+
raise ValueError("n must be >= 1")
|
|
141
|
+
|
|
142
|
+
# Build a new randomized limited query leaving self.target untouched
|
|
143
|
+
randomized = self.target.order_by(sm.func.random()).limit(n)
|
|
144
|
+
|
|
145
|
+
with get_session() as session:
|
|
146
|
+
result = list(session.exec(randomized))
|
|
147
|
+
|
|
148
|
+
if n == 1:
|
|
149
|
+
# Return the single instance or None
|
|
150
|
+
return result[0] if result else None
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
98
154
|
def __repr__(self) -> str:
|
|
99
155
|
# TODO we should improve structure of this a bit more, maybe wrap in <> or something?
|
|
100
156
|
return f"{self.__class__.__name__}: Current SQL:\n{self.sql()}"
|
activemodel/session_manager.py
CHANGED
|
@@ -150,6 +150,14 @@ def global_session(session: Session | None = None):
|
|
|
150
150
|
This may only be called a single time per callstack. There is one exception: if you call this multiple times
|
|
151
151
|
and pass in the same session reference, it will result in a noop.
|
|
152
152
|
|
|
153
|
+
In complex testing code, you'll need to be careful here. For example:
|
|
154
|
+
|
|
155
|
+
- Unit test using a transaction db fixture (which sets __sqlalchemy_session__)
|
|
156
|
+
- Factory has a after_save hook
|
|
157
|
+
- That hook triggers a celery job
|
|
158
|
+
- The celery job (properly) calls `with global_session()`
|
|
159
|
+
- However, since `global_session()` is already set with __sqlalchemy_session__, this will raise an error
|
|
160
|
+
|
|
153
161
|
Args:
|
|
154
162
|
session: Use an existing session instead of creating a new one
|
|
155
163
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: activemodel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Make SQLModel more like an a real ORM
|
|
5
5
|
Project-URL: Repository, https://github.com/iloveitaly/activemodel
|
|
6
6
|
Author-email: Michael Bianco <iloveitaly@gmail.com>
|
|
@@ -290,7 +290,8 @@ https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d
|
|
|
290
290
|
|
|
291
291
|
* https://github.com/woofz/sqlmodel-basecrud
|
|
292
292
|
* https://github.com/0xthiagomartins/sqlmodel-controller
|
|
293
|
-
* https://github.com/litestar-org/advanced-alchemy
|
|
293
|
+
* https://github.com/litestar-org/advanced-alchemy
|
|
294
|
+
* https://github.com/dialoguemd/fastapi-sqla
|
|
294
295
|
|
|
295
296
|
## Inspiration
|
|
296
297
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
activemodel/__init__.py,sha256=q_lHQyIM70ApvjduTo9GtenQjJXsfYZsAAquD_51kF4,137
|
|
2
|
-
activemodel/base_model.py,sha256=
|
|
2
|
+
activemodel/base_model.py,sha256=0QRs2C_QtJFx6voSWr0jFXsbEgtn_PmYA2rrwejVzCU,17496
|
|
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
|
|
6
6
|
activemodel/logger.py,sha256=vU7QiGSy_AJuJFmClUocqIJ-Ltku_8C24ZU8L6fLJR0,53
|
|
7
|
-
activemodel/query_wrapper.py,sha256=
|
|
8
|
-
activemodel/session_manager.py,sha256=
|
|
7
|
+
activemodel/query_wrapper.py,sha256=DLfmpMQr5veBjQIU2KEsp7Pe3MvdxQ-R-C6tkj0SgU8,4832
|
|
8
|
+
activemodel/session_manager.py,sha256=4jK0rs3KxU84WOvHCX6iuy0mauJg02tQ2VUQj-OEk68,7124
|
|
9
9
|
activemodel/utils.py,sha256=tZlAk0G46g6dwYuN7dIr8xU9QC_aLZYqjDXYkGiCtUg,888
|
|
10
10
|
activemodel/cli/__init__.py,sha256=HrgJjB5pRuE6hbwgy0Dw4oHvGZ47kH0LPVAdG9l6-vw,5021
|
|
11
11
|
activemodel/mixins/__init__.py,sha256=05EQl2u_Wgf_wkly-GTaTsR7zWpmpKcb96Js7r_rZTw,160
|
|
12
|
-
activemodel/mixins/pydantic_json.py,sha256=
|
|
12
|
+
activemodel/mixins/pydantic_json.py,sha256=Nm8Y0ra7N-2lEvLHtmZYq1XE1-4n2BIff812DzKgOb4,4461
|
|
13
13
|
activemodel/mixins/soft_delete.py,sha256=Ax4mGsQI7AVTE8c4GiWxpyB_W179-dDct79GtjP0owU,461
|
|
14
14
|
activemodel/mixins/timestamps.py,sha256=C6QQNnzrNUOW1EAsMpEVpImEeTIYDMPP0wocEw2RDQw,1078
|
|
15
15
|
activemodel/mixins/typeid.py,sha256=777btWRUW6YBGPApeaEdHQaoKmwblehukHzmkKoXv6o,1340
|
|
@@ -23,8 +23,8 @@ activemodel/types/sqlalchemy_protocol.py,sha256=2MSuGIp6pcIyiy8uK7qX3FLWABBMQOJG
|
|
|
23
23
|
activemodel/types/sqlalchemy_protocol.pyi,sha256=SP4Z50SGcw6qSexGgNd_4g6E_sQwpIE44vgNT4ncmeI,5667
|
|
24
24
|
activemodel/types/typeid.py,sha256=qycqklKv5nKuCqjJRnxA-6MjtcWJ4vFUsAVBc1ySwfg,7865
|
|
25
25
|
activemodel/types/typeid_patch.py,sha256=y6kiCJQ_NzeKfuI4UtRAs7QW_nEog5RIA_-k4HUBMkU,575
|
|
26
|
-
activemodel-0.
|
|
27
|
-
activemodel-0.
|
|
28
|
-
activemodel-0.
|
|
29
|
-
activemodel-0.
|
|
30
|
-
activemodel-0.
|
|
26
|
+
activemodel-0.14.0.dist-info/METADATA,sha256=pnDgnDPyY5Nz0ooqF0rrxe1DE7JcFFKcMqXbO-6aGOs,10750
|
|
27
|
+
activemodel-0.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
28
|
+
activemodel-0.14.0.dist-info/entry_points.txt,sha256=rytVrsNgUT4oDiW9RvRH6JBTHQn0hPZLK-jzQt3dY9s,51
|
|
29
|
+
activemodel-0.14.0.dist-info/licenses/LICENSE,sha256=L8mmpX47rB-xtJ_HsK0zpfO6viEjxbLYGn70BMp8os4,1071
|
|
30
|
+
activemodel-0.14.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|