activemodel 0.9.0__py3-none-any.whl → 0.10.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 +53 -7
- activemodel/mixins/typeid.py +12 -25
- activemodel/session_manager.py +7 -0
- {activemodel-0.9.0.dist-info → activemodel-0.10.0.dist-info}/METADATA +1 -1
- {activemodel-0.9.0.dist-info → activemodel-0.10.0.dist-info}/RECORD +8 -8
- {activemodel-0.9.0.dist-info → activemodel-0.10.0.dist-info}/WHEEL +0 -0
- {activemodel-0.9.0.dist-info → activemodel-0.10.0.dist-info}/entry_points.txt +0 -0
- {activemodel-0.9.0.dist-info → activemodel-0.10.0.dist-info}/licenses/LICENSE +0 -0
activemodel/base_model.py
CHANGED
|
@@ -4,11 +4,11 @@ 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
|
|
9
7
|
import sqlmodel as sm
|
|
10
8
|
from sqlalchemy import Connection, event
|
|
11
9
|
from sqlalchemy.orm import Mapper, declared_attr
|
|
10
|
+
from sqlalchemy.orm.attributes import flag_modified as sa_flag_modified
|
|
11
|
+
from sqlalchemy.orm.base import instance_state
|
|
12
12
|
from sqlmodel import Column, Field, Session, SQLModel, inspect, select
|
|
13
13
|
from typeid import TypeID
|
|
14
14
|
|
|
@@ -19,6 +19,7 @@ from . import get_column_from_field_patch # noqa: F401
|
|
|
19
19
|
from .logger import logger
|
|
20
20
|
from .query_wrapper import QueryWrapper
|
|
21
21
|
from .session_manager import get_session
|
|
22
|
+
from sqlalchemy.dialects.postgresql import insert as postgres_insert
|
|
22
23
|
|
|
23
24
|
POSTGRES_INDEXES_NAMING_CONVENTION = {
|
|
24
25
|
"ix": "%(column_0_label)s_idx",
|
|
@@ -137,8 +138,15 @@ class BaseModel(SQLModel):
|
|
|
137
138
|
cls.__table_args__ = {"comment": doc}
|
|
138
139
|
elif isinstance(table_args, dict):
|
|
139
140
|
table_args.setdefault("comment", doc)
|
|
141
|
+
elif isinstance(table_args, tuple):
|
|
142
|
+
# If it's a tuple, we need to convert it to a list and add the comment
|
|
143
|
+
table_args = list(table_args)
|
|
144
|
+
table_args.append({"comment": doc})
|
|
145
|
+
cls.__table_args__ = tuple(table_args)
|
|
140
146
|
else:
|
|
141
|
-
raise ValueError(
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Unexpected __table_args__ type {type(table_args)}, expected dictionary."
|
|
149
|
+
)
|
|
142
150
|
|
|
143
151
|
# TODO no type check decorator here
|
|
144
152
|
@declared_attr
|
|
@@ -163,8 +171,10 @@ class BaseModel(SQLModel):
|
|
|
163
171
|
"""
|
|
164
172
|
Returns a `Field` object referencing the foreign key of the model.
|
|
165
173
|
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
Helps quickly build a many-to-one or one-to-one relationship.
|
|
175
|
+
|
|
176
|
+
>>> other_model_id: int = OtherModel.foreign_key()
|
|
177
|
+
>>> other_model = Relationship()
|
|
168
178
|
"""
|
|
169
179
|
|
|
170
180
|
field_options = {"nullable": False} | kwargs
|
|
@@ -186,6 +196,42 @@ class BaseModel(SQLModel):
|
|
|
186
196
|
"convenience method to avoid having to write .select().where() in order to add conditions"
|
|
187
197
|
return cls.select().where(*args)
|
|
188
198
|
|
|
199
|
+
@classmethod
|
|
200
|
+
def upsert(
|
|
201
|
+
cls,
|
|
202
|
+
data: dict[str, t.Any],
|
|
203
|
+
unique_by: str | list[str],
|
|
204
|
+
) -> None:
|
|
205
|
+
"""
|
|
206
|
+
This method will insert a new record if it doesn't exist, or update the existing record if it does.
|
|
207
|
+
|
|
208
|
+
It uses SQLAlchemy's `on_conflict_do_update` and does not yet support MySQL. Some implementation details below.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
- `index_elements=["name"]`: Specifies the column(s) to check for conflicts (e.g., unique constraint or index). If a row with the same "name" exists, it triggers the update instead of an insert.
|
|
213
|
+
- `values`: Defines the data to insert (e.g., `name="example", value=123`). If no conflict occurs, this data is inserted as a new row.
|
|
214
|
+
|
|
215
|
+
The `set_` parameter (e.g., `set_=dict(value=123)`) then dictates what gets updated on conflict, overriding matching fields in `values` if specified.
|
|
216
|
+
"""
|
|
217
|
+
index_elements = [unique_by] if isinstance(unique_by, str) else unique_by
|
|
218
|
+
|
|
219
|
+
stmt = (
|
|
220
|
+
postgres_insert(cls)
|
|
221
|
+
.values(**data)
|
|
222
|
+
.on_conflict_do_update(index_elements=index_elements, set_=data)
|
|
223
|
+
.returning(cls)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
with get_session() as session:
|
|
227
|
+
result = session.exec(stmt)
|
|
228
|
+
session.commit()
|
|
229
|
+
|
|
230
|
+
# TODO this is so ugly:
|
|
231
|
+
result = result.one()[0]
|
|
232
|
+
|
|
233
|
+
return result
|
|
234
|
+
|
|
189
235
|
def delete(self):
|
|
190
236
|
with get_session() as session:
|
|
191
237
|
if old_session := Session.object_session(self):
|
|
@@ -256,9 +302,9 @@ class BaseModel(SQLModel):
|
|
|
256
302
|
def is_new(self) -> bool:
|
|
257
303
|
return not self._sa_instance_state.has_identity
|
|
258
304
|
|
|
259
|
-
def flag_modified(self, *args: str):
|
|
305
|
+
def flag_modified(self, *args: str) -> None:
|
|
260
306
|
"""
|
|
261
|
-
Flag one or more fields as modified. Useful for marking a field containing sub-objects as modified.
|
|
307
|
+
Flag one or more fields as modified/mutated/dirty. Useful for marking a field containing sub-objects as modified.
|
|
262
308
|
|
|
263
309
|
Will throw an error if an invalid field is passed.
|
|
264
310
|
"""
|
activemodel/mixins/typeid.py
CHANGED
|
@@ -4,43 +4,30 @@ from typeid import TypeID
|
|
|
4
4
|
from activemodel.types.typeid import TypeIDType
|
|
5
5
|
|
|
6
6
|
# global list of prefixes to ensure uniqueness
|
|
7
|
-
_prefixes = []
|
|
7
|
+
_prefixes: list[str] = []
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def TypeIDMixin(prefix: str):
|
|
11
|
+
# make sure duplicate prefixes are not used!
|
|
12
|
+
# NOTE this will cause issues on code reloads
|
|
11
13
|
assert prefix
|
|
12
14
|
assert prefix not in _prefixes, (
|
|
13
15
|
f"prefix {prefix} already exists, pick a different one"
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
class _TypeIDMixin:
|
|
19
|
+
__abstract__ = True
|
|
20
|
+
|
|
17
21
|
id: TypeIDType = Field(
|
|
18
|
-
sa_column=Column(
|
|
19
|
-
|
|
22
|
+
sa_column=Column(
|
|
23
|
+
TypeIDType(prefix),
|
|
24
|
+
primary_key=True,
|
|
25
|
+
nullable=False,
|
|
26
|
+
default=lambda: TypeID(prefix),
|
|
27
|
+
),
|
|
28
|
+
# default_factory=lambda: TypeID(prefix),
|
|
20
29
|
)
|
|
21
30
|
|
|
22
31
|
_prefixes.append(prefix)
|
|
23
32
|
|
|
24
33
|
return _TypeIDMixin
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# TODO not sure if I love the idea of a dynamic class for each mixin as used above
|
|
28
|
-
# may give this approach another shot in the future
|
|
29
|
-
# class TypeIDMixin2:
|
|
30
|
-
# """
|
|
31
|
-
# Mixin class that adds a TypeID primary key to models.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# >>> class MyModel(BaseModel, TypeIDMixin, prefix="xyz", table=True):
|
|
35
|
-
# >>> name: str
|
|
36
|
-
|
|
37
|
-
# Will automatically have an `id` field with prefix "xyz"
|
|
38
|
-
# """
|
|
39
|
-
|
|
40
|
-
# def __init_subclass__(cls, *, prefix: str, **kwargs):
|
|
41
|
-
# super().__init_subclass__(**kwargs)
|
|
42
|
-
|
|
43
|
-
# cls.id: uuid.UUID = Field(
|
|
44
|
-
# sa_column=Column(TypeIDType(prefix), primary_key=True),
|
|
45
|
-
# default_factory=lambda: TypeID(prefix),
|
|
46
|
-
# )
|
activemodel/session_manager.py
CHANGED
|
@@ -72,6 +72,7 @@ class SessionManager:
|
|
|
72
72
|
self._database_url,
|
|
73
73
|
# NOTE very important! This enables pydantic models to be serialized for JSONB columns
|
|
74
74
|
json_serializer=_serialize_pydantic_model,
|
|
75
|
+
# TODO move to a constants area
|
|
75
76
|
echo=config("ACTIVEMODEL_LOG_SQL", cast=bool, default=False),
|
|
76
77
|
# https://docs.sqlalchemy.org/en/20/core/pooling.html#disconnect-handling-pessimistic
|
|
77
78
|
pool_pre_ping=True,
|
|
@@ -119,6 +120,9 @@ _session_context = contextvars.ContextVar[Session | None](
|
|
|
119
120
|
|
|
120
121
|
@contextlib.contextmanager
|
|
121
122
|
def global_session():
|
|
123
|
+
if _session_context.get() is not None:
|
|
124
|
+
raise RuntimeError("global session already set")
|
|
125
|
+
|
|
122
126
|
with SessionManager.get_instance().get_session() as s:
|
|
123
127
|
token = _session_context.set(s)
|
|
124
128
|
|
|
@@ -140,6 +144,9 @@ async def aglobal_session():
|
|
|
140
144
|
>>> )
|
|
141
145
|
"""
|
|
142
146
|
|
|
147
|
+
if _session_context.get() is not None:
|
|
148
|
+
raise RuntimeError("global session already set")
|
|
149
|
+
|
|
143
150
|
with SessionManager.get_instance().get_session() as s:
|
|
144
151
|
token = _session_context.set(s)
|
|
145
152
|
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
activemodel/__init__.py,sha256=q_lHQyIM70ApvjduTo9GtenQjJXsfYZsAAquD_51kF4,137
|
|
2
|
-
activemodel/base_model.py,sha256=
|
|
2
|
+
activemodel/base_model.py,sha256=VSItKKMxP-g-en_v16VnR9W6ueSLlWqmxn7I2-TgpGk,16646
|
|
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
7
|
activemodel/query_wrapper.py,sha256=rNdvueppMse2MIi-RafTEC34GPGRal_wqH2CzhmlWS8,2520
|
|
8
|
-
activemodel/session_manager.py,sha256=
|
|
8
|
+
activemodel/session_manager.py,sha256=ltmUyBsYCNNddoilLWrh3HX9QY9eQSZiRsyFf0awevs,4835
|
|
9
9
|
activemodel/utils.py,sha256=g17UqkphzTmb6YdpmYwT1TM00eDiXXuWn39-xNiu0AA,2112
|
|
10
10
|
activemodel/mixins/__init__.py,sha256=05EQl2u_Wgf_wkly-GTaTsR7zWpmpKcb96Js7r_rZTw,160
|
|
11
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
|
-
activemodel/mixins/typeid.py,sha256=
|
|
14
|
+
activemodel/mixins/typeid.py,sha256=WBZwnryF2QkI1ki0fW-jEbE8cIqMIldwkaeJdGT01S4,841
|
|
15
15
|
activemodel/pytest/__init__.py,sha256=W9KKQHbPkyq0jrMXaiL8hG2Nsbjy_LN9HhvgGm8W_7g,98
|
|
16
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
19
|
activemodel/types/typeid.py,sha256=1xB79DGIC5-P-PcLpeZW9Ed_WjFOmmVW1yl2Q3pPJis,7250
|
|
20
|
-
activemodel-0.
|
|
21
|
-
activemodel-0.
|
|
22
|
-
activemodel-0.
|
|
23
|
-
activemodel-0.
|
|
24
|
-
activemodel-0.
|
|
20
|
+
activemodel-0.10.0.dist-info/METADATA,sha256=rin3Edbj6CFW8-cj6fcL6S6GU8a1Qn0Sl9tGOLM-_rw,9652
|
|
21
|
+
activemodel-0.10.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
22
|
+
activemodel-0.10.0.dist-info/entry_points.txt,sha256=YLX62TP_hR-n3HMBkdBex4W7XRiyOtIPkwy22puIjjQ,61
|
|
23
|
+
activemodel-0.10.0.dist-info/licenses/LICENSE,sha256=L8mmpX47rB-xtJ_HsK0zpfO6viEjxbLYGn70BMp8os4,1071
|
|
24
|
+
activemodel-0.10.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|