activemodel 0.5.0__py3-none-any.whl → 0.8.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/__init__.py +2 -0
- activemodel/base_model.py +141 -33
- activemodel/celery.py +33 -0
- activemodel/errors.py +6 -0
- activemodel/get_column_from_field_patch.py +139 -0
- activemodel/mixins/__init__.py +2 -0
- activemodel/mixins/pydantic_json.py +82 -0
- activemodel/mixins/soft_delete.py +17 -0
- activemodel/mixins/typeid.py +27 -17
- activemodel/pytest/transaction.py +34 -22
- activemodel/pytest/truncate.py +1 -1
- activemodel/query_wrapper.py +24 -10
- activemodel/session_manager.py +92 -5
- activemodel/types/__init__.py +1 -0
- activemodel/types/typeid.py +141 -5
- activemodel/utils.py +51 -1
- activemodel-0.8.0.dist-info/METADATA +282 -0
- activemodel-0.8.0.dist-info/RECORD +24 -0
- {activemodel-0.5.0.dist-info → activemodel-0.8.0.dist-info}/WHEEL +1 -2
- activemodel/_session_manager.py +0 -153
- activemodel-0.5.0.dist-info/METADATA +0 -66
- activemodel-0.5.0.dist-info/RECORD +0 -20
- activemodel-0.5.0.dist-info/top_level.txt +0 -1
- {activemodel-0.5.0.dist-info → activemodel-0.8.0.dist-info}/entry_points.txt +0 -0
- {activemodel-0.5.0.dist-info → activemodel-0.8.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: activemodel
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Make SQLModel more like an a real ORM
|
|
5
|
+
Project-URL: Repository, https://github.com/iloveitaly/activemodel
|
|
6
|
+
Author-email: Michael Bianco <iloveitaly@gmail.com>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: activemodel,activerecord,orm,sqlalchemy,sqlmodel
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: pydash>=8.0.4
|
|
11
|
+
Requires-Dist: python-decouple-typed>=3.11.0
|
|
12
|
+
Requires-Dist: sqlmodel>=0.0.22
|
|
13
|
+
Requires-Dist: typeid-python>=0.3.1
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# ActiveModel: ORM Wrapper for SQLModel
|
|
17
|
+
|
|
18
|
+
No, this isn't *really* [ActiveModel](https://guides.rubyonrails.org/active_model_basics.html). It's just a wrapper around SQLModel that provides a more ActiveRecord-like interface.
|
|
19
|
+
|
|
20
|
+
SQLModel is *not* an ORM. It's a SQL query builder and a schema definition tool.
|
|
21
|
+
|
|
22
|
+
This package provides a thin wrapper around SQLModel that provides a more ActiveRecord-like interface with things like:
|
|
23
|
+
|
|
24
|
+
* Timestamp column mixins
|
|
25
|
+
* Lifecycle hooks
|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
First, setup your DB:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import activemodel
|
|
33
|
+
activemodel.init("sqlite:///database.db")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Create models:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from activemodel import BaseModel
|
|
40
|
+
from activemodel.mixins import TimestampsMixin, TypeIDMixin
|
|
41
|
+
|
|
42
|
+
class User(
|
|
43
|
+
BaseModel,
|
|
44
|
+
# optionally, obviously
|
|
45
|
+
TimestampsMixin,
|
|
46
|
+
# you can use a different pk type, but why would you?
|
|
47
|
+
# put this mixin last otherwise `id` will not be the first column in the DB
|
|
48
|
+
TypeIDMixin("user"),
|
|
49
|
+
# wire this model into the DB, without this alembic will not generate a migration
|
|
50
|
+
table=True
|
|
51
|
+
):
|
|
52
|
+
a_field: str
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
You'll need to create the models in the DB. Alembic is the best way to do it, but you can cheat as well:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from sqlmodel import SQLModel
|
|
59
|
+
|
|
60
|
+
SQLModel.metadata.create_all(get_engine())
|
|
61
|
+
|
|
62
|
+
# now you can create a user!
|
|
63
|
+
User(a_field="a").save()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Maybe you like JSON:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from activemodel import BaseModel
|
|
70
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
71
|
+
from activemodel.mixins import PydanticJSONMixin, TypeIDMixin, TimestampsMixin
|
|
72
|
+
|
|
73
|
+
class SubObject(PydanticBaseModel):
|
|
74
|
+
name: str
|
|
75
|
+
value: int
|
|
76
|
+
|
|
77
|
+
class User(
|
|
78
|
+
BaseModel,
|
|
79
|
+
TimestampsMixin,
|
|
80
|
+
PydanticJSONMixin,
|
|
81
|
+
TypeIDMixin("user"),
|
|
82
|
+
table=True
|
|
83
|
+
):
|
|
84
|
+
list_field: list[SubObject] = Field(sa_type=JSONB())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
### Integrating Alembic
|
|
90
|
+
|
|
91
|
+
`alembic init` will not work out of the box. You need to mutate a handful of files:
|
|
92
|
+
|
|
93
|
+
* To import all of your models you want in your DB. [Here's my recommended way to do this.](https://github.com/iloveitaly/python-starter-template/blob/master/app/models/__init__.py)
|
|
94
|
+
* Use your DB URL from the ENV
|
|
95
|
+
* Target sqlalchemy metadata to the sqlmodel-generated metadata
|
|
96
|
+
* Most likely you'll want to add [alembic-postgresql-enum](https://pypi.org/project/alembic-postgresql-enum/) so migrations work properly
|
|
97
|
+
|
|
98
|
+
[Take a look at these scripts for an example of how to fully integrate Alembic into your development workflow.](https://github.com/iloveitaly/python-starter-template/blob/0af2c7e95217e34bde7357cc95be048900000e48/Justfile#L618-L712)
|
|
99
|
+
|
|
100
|
+
Here's a diff from the bare `alembic init` from version `1.14.1`.
|
|
101
|
+
|
|
102
|
+
```diff
|
|
103
|
+
diff --git i/test/migrations/alembic.ini w/test/migrations/alembic.ini
|
|
104
|
+
index 0d07420..a63631c 100644
|
|
105
|
+
--- i/test/migrations/alembic.ini
|
|
106
|
+
+++ w/test/migrations/alembic.ini
|
|
107
|
+
@@ -3,13 +3,14 @@
|
|
108
|
+
[alembic]
|
|
109
|
+
# path to migration scripts
|
|
110
|
+
# Use forward slashes (/) also on windows to provide an os agnostic path
|
|
111
|
+
-script_location = .
|
|
112
|
+
+script_location = migrations
|
|
113
|
+
|
|
114
|
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
115
|
+
# Uncomment the line below if you want the files to be prepended with date and time
|
|
116
|
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
|
117
|
+
# for all available tokens
|
|
118
|
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
119
|
+
+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
|
|
120
|
+
|
|
121
|
+
# sys.path path, will be prepended to sys.path if present.
|
|
122
|
+
# defaults to the current working directory.
|
|
123
|
+
diff --git i/test/migrations/env.py w/test/migrations/env.py
|
|
124
|
+
index 36112a3..a1e15c2 100644
|
|
125
|
+
--- i/test/migrations/env.py
|
|
126
|
+
+++ w/test/migrations/env.py
|
|
127
|
+
@@ -1,3 +1,6 @@
|
|
128
|
+
+# fmt: off
|
|
129
|
+
+# isort: off
|
|
130
|
+
+
|
|
131
|
+
from logging.config import fileConfig
|
|
132
|
+
|
|
133
|
+
from sqlalchemy import engine_from_config
|
|
134
|
+
@@ -14,11 +17,17 @@ config = context.config
|
|
135
|
+
if config.config_file_name is not None:
|
|
136
|
+
fileConfig(config.config_file_name)
|
|
137
|
+
|
|
138
|
+
+from sqlmodel import SQLModel
|
|
139
|
+
+from test.models import *
|
|
140
|
+
+from test.utils import database_url
|
|
141
|
+
+
|
|
142
|
+
+config.set_main_option("sqlalchemy.url", database_url())
|
|
143
|
+
+
|
|
144
|
+
# add your model's MetaData object here
|
|
145
|
+
# for 'autogenerate' support
|
|
146
|
+
# from myapp import mymodel
|
|
147
|
+
# target_metadata = mymodel.Base.metadata
|
|
148
|
+
-target_metadata = None
|
|
149
|
+
+target_metadata = SQLModel.metadata
|
|
150
|
+
|
|
151
|
+
# other values from the config, defined by the needs of env.py,
|
|
152
|
+
# can be acquired:
|
|
153
|
+
diff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako
|
|
154
|
+
index fbc4b07..9dc78bb 100644
|
|
155
|
+
--- i/test/migrations/script.py.mako
|
|
156
|
+
+++ w/test/migrations/script.py.mako
|
|
157
|
+
@@ -9,6 +9,8 @@ from typing import Sequence, Union
|
|
158
|
+
|
|
159
|
+
from alembic import op
|
|
160
|
+
import sqlalchemy as sa
|
|
161
|
+
+import sqlmodel
|
|
162
|
+
+import activemodel
|
|
163
|
+
${imports if imports else ""}
|
|
164
|
+
|
|
165
|
+
# revision identifiers, used by Alembic.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Here are some useful resources around Alembic + SQLModel:
|
|
169
|
+
|
|
170
|
+
* https://github.com/fastapi/sqlmodel/issues/85
|
|
171
|
+
* https://testdriven.io/blog/fastapi-sqlmodel/
|
|
172
|
+
|
|
173
|
+
### Query Wrapper
|
|
174
|
+
|
|
175
|
+
This tool is added to all `BaseModel`s and makes it easy to write SQL queries. Some examples:
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
### Easy Database Sessions
|
|
180
|
+
|
|
181
|
+
I hate the idea f
|
|
182
|
+
|
|
183
|
+
* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.
|
|
184
|
+
* Don't worry about dead sessions. This makes it easy to lazy-load computed properties and largely eliminates the need to think about database sessions.
|
|
185
|
+
|
|
186
|
+
There are a couple of thorny problems we need to solve for here:
|
|
187
|
+
|
|
188
|
+
* In-memory fastapi servers are not the same as a uvicorn server, which is threaded *and* uses some sort of threadpool model for handling async requests. I don't claim to understand the entire implementation. For global DB session state (a) we can't use global variables (b) we can't use thread-local variables.
|
|
189
|
+
*
|
|
190
|
+
|
|
191
|
+
https://github.com/tomwojcik/starlette-context
|
|
192
|
+
|
|
193
|
+
### Example Queries
|
|
194
|
+
|
|
195
|
+
* Conditional: `Scrape.select().where(Scrape.id < last_scraped.id).all()`
|
|
196
|
+
* Equality: `MenuItem.select().where(MenuItem.menu_id == menu.id).all()`
|
|
197
|
+
* `IN` example: `CanonicalMenuItem.select().where(col(CanonicalMenuItem.id).in_(canonized_ids)).all()`
|
|
198
|
+
* Compound where query: `User.where((User.last_active_at != None) & (User.last_active_at > last_24_hours)).count()`
|
|
199
|
+
|
|
200
|
+
### SQLModel Internals
|
|
201
|
+
|
|
202
|
+
SQLModel & SQLAlchemy are tricky. Here are some useful internal tricks:
|
|
203
|
+
|
|
204
|
+
* `__sqlmodel_relationships__` is where any `RelationshipInfo` objects are stored. This is used to generate relationship fields on the object.
|
|
205
|
+
* `ModelClass.relationship_name.property.local_columns`
|
|
206
|
+
* Get cached fields from a model `object_state(instance).dict.get(field_name)`
|
|
207
|
+
|
|
208
|
+
### TypeID
|
|
209
|
+
|
|
210
|
+
I'm a massive fan of Stripe-style prefixed UUIDs. [There's an excellent project](https://github.com/jetify-com/typeid)
|
|
211
|
+
that defined a clear spec for these IDs. I've used the python implementation of this spec and developed a clean integration
|
|
212
|
+
with SQLModel that plays well with fastapi as well.
|
|
213
|
+
|
|
214
|
+
Here's an example of defining a relationship:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import uuid
|
|
218
|
+
|
|
219
|
+
from activemodel import BaseModel
|
|
220
|
+
from activemodel.mixins import TimestampsMixin, TypeIDMixin
|
|
221
|
+
from activemodel.types import TypeIDType
|
|
222
|
+
from sqlmodel import Field, Relationship
|
|
223
|
+
|
|
224
|
+
from .patient import Patient
|
|
225
|
+
|
|
226
|
+
class Appointment(
|
|
227
|
+
BaseModel,
|
|
228
|
+
# this adds an `id` field to the model with the correct type
|
|
229
|
+
TypeIDMixin("appointment"),
|
|
230
|
+
table=True
|
|
231
|
+
):
|
|
232
|
+
# `foreign_key` is a activemodel method to generate the right `Field` for the relationship
|
|
233
|
+
# TypeIDType is really important here for fastapi serialization
|
|
234
|
+
doctor_id: TypeIDType = Doctor.foreign_key()
|
|
235
|
+
doctor: Doctor = Relationship()
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Limitations
|
|
239
|
+
|
|
240
|
+
### Validation
|
|
241
|
+
|
|
242
|
+
SQLModel does not currently support pydantic validations (when `table=True`). This is very surprising, but is actually the intended functionality:
|
|
243
|
+
|
|
244
|
+
* https://github.com/fastapi/sqlmodel/discussions/897
|
|
245
|
+
* https://github.com/fastapi/sqlmodel/pull/1041
|
|
246
|
+
* https://github.com/fastapi/sqlmodel/issues/453
|
|
247
|
+
* https://github.com/fastapi/sqlmodel/issues/52#issuecomment-1311987732
|
|
248
|
+
|
|
249
|
+
For validation:
|
|
250
|
+
|
|
251
|
+
* When consuming API data, use a separate shadow model to validate the data with `table=False` and then inherit from that model in a model with `table=True`.
|
|
252
|
+
* When validating ORM data, use SQL Alchemy hooks.
|
|
253
|
+
|
|
254
|
+
<!--
|
|
255
|
+
|
|
256
|
+
This looks neat
|
|
257
|
+
https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L155
|
|
258
|
+
schema_extra={
|
|
259
|
+
'pattern': r'^[a-z0-9_\-\.]+\@[a-z0-9_\-\.]+\.[a-z\.]+$'
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
extra constraints
|
|
263
|
+
|
|
264
|
+
https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L424C1-L426C6
|
|
265
|
+
-->
|
|
266
|
+
## Related Projects
|
|
267
|
+
|
|
268
|
+
* https://github.com/woofz/sqlmodel-basecrud
|
|
269
|
+
* https://github.com/0xthiagomartins/sqlmodel-controller
|
|
270
|
+
|
|
271
|
+
## Inspiration
|
|
272
|
+
|
|
273
|
+
* https://github.com/peterdresslar/fastapi-sqlmodel-alembic-pg
|
|
274
|
+
* [Albemic instructions](https://github.com/fastapi/sqlmodel/pull/899/files)
|
|
275
|
+
* https://github.com/fastapiutils/fastapi-utils/
|
|
276
|
+
* https://github.com/fastapi/full-stack-fastapi-template
|
|
277
|
+
* https://github.com/DarylStark/my_data/
|
|
278
|
+
* https://github.com/petrgazarov/FastAPI-app/tree/main/fastapi_app
|
|
279
|
+
|
|
280
|
+
## Upstream Changes
|
|
281
|
+
|
|
282
|
+
- [ ] https://github.com/fastapi/sqlmodel/pull/1293
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
activemodel/__init__.py,sha256=q_lHQyIM70ApvjduTo9GtenQjJXsfYZsAAquD_51kF4,137
|
|
2
|
+
activemodel/base_model.py,sha256=MM-TRlf_0DnCZHeNWxQ1S-VMq8JSBYybPfhtLZHZeRE,12066
|
|
3
|
+
activemodel/celery.py,sha256=L1vKcO_HoPA5ZCfsXjxgPpDUMYDuoQMakGA9rppN7Lo,897
|
|
4
|
+
activemodel/errors.py,sha256=wycWYmk9ws4TZpxvTdtXVy2SFESb8NqKgzdivBoF0vw,115
|
|
5
|
+
activemodel/get_column_from_field_patch.py,sha256=wAEDm_ZvSqyJwfgkXVpxsevw11hd-7VLy7zuJG8Ak7Y,4986
|
|
6
|
+
activemodel/logger.py,sha256=vU7QiGSy_AJuJFmClUocqIJ-Ltku_8C24ZU8L6fLJR0,53
|
|
7
|
+
activemodel/query_wrapper.py,sha256=rNdvueppMse2MIi-RafTEC34GPGRal_wqH2CzhmlWS8,2520
|
|
8
|
+
activemodel/session_manager.py,sha256=Vtg8Lf8vUNPegdRW-fyE-Ng5wtN3hTMfUezdFUiJ1fs,4585
|
|
9
|
+
activemodel/utils.py,sha256=g17UqkphzTmb6YdpmYwT1TM00eDiXXuWn39-xNiu0AA,2112
|
|
10
|
+
activemodel/mixins/__init__.py,sha256=05EQl2u_Wgf_wkly-GTaTsR7zWpmpKcb96Js7r_rZTw,160
|
|
11
|
+
activemodel/mixins/pydantic_json.py,sha256=8A5X6QVzMSvDkBIb1impZ9PYskkUviG1UW7kkoxI8Wg,3057
|
|
12
|
+
activemodel/mixins/soft_delete.py,sha256=Ax4mGsQI7AVTE8c4GiWxpyB_W179-dDct79GtjP0owU,461
|
|
13
|
+
activemodel/mixins/timestamps.py,sha256=Q-IFljeVVJQqw3XHdOi7dkqzefiVg1zhJvq_bldpmjg,992
|
|
14
|
+
activemodel/mixins/typeid.py,sha256=DGjlIg8PRBYoaBbWkkxc6jkScyl-p53KuSR98lLgAvE,1284
|
|
15
|
+
activemodel/pytest/__init__.py,sha256=W9KKQHbPkyq0jrMXaiL8hG2Nsbjy_LN9HhvgGm8W_7g,98
|
|
16
|
+
activemodel/pytest/transaction.py,sha256=GfUpGUiTHATooVfxU3FMF28FHljBVdfVcb51g2KMzhY,2593
|
|
17
|
+
activemodel/pytest/truncate.py,sha256=IGiPLkUm2yyOKww6c6CKcVbwi2xAAFBopx9q2ABfu8w,1582
|
|
18
|
+
activemodel/types/__init__.py,sha256=y5fiGVtPJxGEhuf-TvyrkhM2yaKRcIWo6XAx-CFFjM8,31
|
|
19
|
+
activemodel/types/typeid.py,sha256=XrwCMvAkoZSeM5WhKH-aGJeiK0e9HoTXCheEDUgBBgQ,7292
|
|
20
|
+
activemodel-0.8.0.dist-info/METADATA,sha256=niH50sWYcT1c8eprDrEhetodN2bx_otnMsuzpKQZAAk,9651
|
|
21
|
+
activemodel-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
22
|
+
activemodel-0.8.0.dist-info/entry_points.txt,sha256=YLX62TP_hR-n3HMBkdBex4W7XRiyOtIPkwy22puIjjQ,61
|
|
23
|
+
activemodel-0.8.0.dist-info/licenses/LICENSE,sha256=L8mmpX47rB-xtJ_HsK0zpfO6viEjxbLYGn70BMp8os4,1071
|
|
24
|
+
activemodel-0.8.0.dist-info/RECORD,,
|
activemodel/_session_manager.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Adapted from: https://github.com/fastapiutils/fastapi-utils/blob/master/fastapi_utils/session.py
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from contextlib import contextmanager
|
|
6
|
-
|
|
7
|
-
import sqlalchemy as sa
|
|
8
|
-
from sqlalchemy.orm import Session
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class FastSessionMaker:
|
|
12
|
-
"""
|
|
13
|
-
A convenience class for managing a (cached) sqlalchemy ORM engine and sessionmaker.
|
|
14
|
-
|
|
15
|
-
Intended for use creating ORM sessions injected into endpoint functions by FastAPI.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
def __init__(self, database_uri: str):
|
|
19
|
-
"""
|
|
20
|
-
`database_uri` should be any sqlalchemy-compatible database URI.
|
|
21
|
-
|
|
22
|
-
In particular, `sqlalchemy.create_engine(database_uri)` should work to create an engine.
|
|
23
|
-
|
|
24
|
-
Typically, this would look like:
|
|
25
|
-
|
|
26
|
-
"<scheme>://<user>:<password>@<host>:<port>/<database>"
|
|
27
|
-
|
|
28
|
-
A concrete example looks like "postgresql://db_user:password@db:5432/app"
|
|
29
|
-
"""
|
|
30
|
-
self.database_uri = database_uri
|
|
31
|
-
|
|
32
|
-
self._cached_engine: sa.engine.Engine | None = None
|
|
33
|
-
self._cached_sessionmaker: sa.orm.sessionmaker | None = None
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def cached_engine(self) -> sa.engine.Engine:
|
|
37
|
-
"""
|
|
38
|
-
Returns a lazily-cached sqlalchemy engine for the instance's database_uri.
|
|
39
|
-
"""
|
|
40
|
-
engine = self._cached_engine
|
|
41
|
-
if engine is None:
|
|
42
|
-
engine = self.get_new_engine()
|
|
43
|
-
self._cached_engine = engine
|
|
44
|
-
return engine
|
|
45
|
-
|
|
46
|
-
@property
|
|
47
|
-
def cached_sessionmaker(self) -> sa.orm.sessionmaker:
|
|
48
|
-
"""
|
|
49
|
-
Returns a lazily-cached sqlalchemy sessionmaker using the instance's (lazily-cached) engine.
|
|
50
|
-
"""
|
|
51
|
-
sessionmaker = self._cached_sessionmaker
|
|
52
|
-
if sessionmaker is None:
|
|
53
|
-
sessionmaker = self.get_new_sessionmaker(self.cached_engine)
|
|
54
|
-
self._cached_sessionmaker = sessionmaker
|
|
55
|
-
return sessionmaker
|
|
56
|
-
|
|
57
|
-
def get_new_engine(self) -> sa.engine.Engine:
|
|
58
|
-
"""
|
|
59
|
-
Returns a new sqlalchemy engine using the instance's database_uri.
|
|
60
|
-
"""
|
|
61
|
-
return get_engine(self.database_uri)
|
|
62
|
-
|
|
63
|
-
def get_new_sessionmaker(
|
|
64
|
-
self, engine: sa.engine.Engine | None
|
|
65
|
-
) -> sa.orm.sessionmaker:
|
|
66
|
-
"""
|
|
67
|
-
Returns a new sessionmaker for the provided sqlalchemy engine. If no engine is provided, the
|
|
68
|
-
instance's (lazily-cached) engine is used.
|
|
69
|
-
"""
|
|
70
|
-
engine = engine or self.cached_engine
|
|
71
|
-
return get_sessionmaker_for_engine(engine)
|
|
72
|
-
|
|
73
|
-
def get_db(self) -> Iterator[Session]:
|
|
74
|
-
"""
|
|
75
|
-
A generator function that yields a sqlalchemy orm session and cleans up the session once resumed after yielding.
|
|
76
|
-
|
|
77
|
-
Can be used directly as a context-manager FastAPI dependency, or yielded from inside a separate dependency.
|
|
78
|
-
"""
|
|
79
|
-
yield from _get_db(self.cached_sessionmaker)
|
|
80
|
-
|
|
81
|
-
@contextmanager
|
|
82
|
-
def context_session(self) -> Iterator[Session]:
|
|
83
|
-
"""
|
|
84
|
-
A context-manager wrapped version of the `get_db` method.
|
|
85
|
-
|
|
86
|
-
This makes it possible to get a context-managed orm session for the relevant database_uri without
|
|
87
|
-
needing to rely on FastAPI's dependency injection.
|
|
88
|
-
|
|
89
|
-
Usage looks like:
|
|
90
|
-
|
|
91
|
-
session_maker = FastAPISessionMaker(database_uri)
|
|
92
|
-
with session_maker.context_session() as session:
|
|
93
|
-
session.query(...)
|
|
94
|
-
...
|
|
95
|
-
"""
|
|
96
|
-
yield from self.get_db()
|
|
97
|
-
|
|
98
|
-
def reset_cache(self) -> None:
|
|
99
|
-
"""
|
|
100
|
-
Resets the engine and sessionmaker caches.
|
|
101
|
-
|
|
102
|
-
After calling this method, the next time you try to use the cached engine or sessionmaker,
|
|
103
|
-
new ones will be created.
|
|
104
|
-
"""
|
|
105
|
-
self._cached_engine = None
|
|
106
|
-
self._cached_sessionmaker = None
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def get_engine(uri: str) -> sa.engine.Engine:
|
|
110
|
-
"""
|
|
111
|
-
Returns a sqlalchemy engine with pool_pre_ping enabled.
|
|
112
|
-
|
|
113
|
-
This function may be updated over time to reflect recommended engine configuration for use with FastAPI.
|
|
114
|
-
"""
|
|
115
|
-
return sa.create_engine(uri, pool_pre_ping=True)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def get_sessionmaker_for_engine(engine: sa.engine.Engine) -> sa.orm.sessionmaker:
|
|
119
|
-
"""
|
|
120
|
-
Returns a sqlalchemy sessionmaker for the provided engine with recommended configuration settings.
|
|
121
|
-
|
|
122
|
-
This function may be updated over time to reflect recommended sessionmaker configuration for use with FastAPI.
|
|
123
|
-
"""
|
|
124
|
-
return sa.orm.sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@contextmanager
|
|
128
|
-
def context_session(engine: sa.engine.Engine) -> Iterator[Session]:
|
|
129
|
-
"""
|
|
130
|
-
This contextmanager yields a managed session for the provided engine.
|
|
131
|
-
|
|
132
|
-
Usage is similar to `FastAPISessionMaker.context_session`, except that you have to provide the engine to use.
|
|
133
|
-
|
|
134
|
-
A new sessionmaker is created for each call, so the FastAPISessionMaker.context_session
|
|
135
|
-
method may be preferable in performance-sensitive contexts.
|
|
136
|
-
"""
|
|
137
|
-
sessionmaker = get_sessionmaker_for_engine(engine)
|
|
138
|
-
yield from _get_db(sessionmaker)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _get_db(sessionmaker: sa.orm.sessionmaker) -> Iterator[Session]:
|
|
142
|
-
"""
|
|
143
|
-
A generator function that yields an ORM session using the provided sessionmaker, and cleans it up when resumed.
|
|
144
|
-
"""
|
|
145
|
-
session = sessionmaker()
|
|
146
|
-
try:
|
|
147
|
-
yield session
|
|
148
|
-
session.commit()
|
|
149
|
-
except Exception as exc:
|
|
150
|
-
session.rollback()
|
|
151
|
-
raise exc
|
|
152
|
-
finally:
|
|
153
|
-
session.close()
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: activemodel
|
|
3
|
-
Version: 0.5.0
|
|
4
|
-
Summary: Make SQLModel more like an a real ORM
|
|
5
|
-
Author-email: Michael Bianco <iloveitaly@gmail.com>
|
|
6
|
-
Project-URL: Repository, https://github.com/iloveitaly/activemodel
|
|
7
|
-
Keywords: sqlmodel,orm,activerecord,activemodel,sqlalchemy
|
|
8
|
-
Requires-Python: >=3.10
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Requires-Dist: pydash>=8.0.4
|
|
12
|
-
Requires-Dist: python-decouple-typed>=3.11.0
|
|
13
|
-
Requires-Dist: sqlmodel>=0.0.22
|
|
14
|
-
Requires-Dist: typeid-python>=0.3.1
|
|
15
|
-
|
|
16
|
-
# ActiveModel: ORM Wrapper for SQLModel
|
|
17
|
-
|
|
18
|
-
No, this isn't *really* [ActiveModel](https://guides.rubyonrails.org/active_model_basics.html). It's just a wrapper around SQLModel that provides a more ActiveRecord-like interface.
|
|
19
|
-
|
|
20
|
-
SQLModel is *not* an ORM. It's a SQL query builder and a schema definition tool.
|
|
21
|
-
|
|
22
|
-
This package provides a thin wrapper around SQLModel that provides a more ActiveRecord-like interface with things like:
|
|
23
|
-
|
|
24
|
-
* Timestamp column mixins
|
|
25
|
-
* Lifecycle hooks
|
|
26
|
-
|
|
27
|
-
## Limitations
|
|
28
|
-
|
|
29
|
-
### Validation
|
|
30
|
-
|
|
31
|
-
SQLModel does not currently support pydantic validations (when `table=True`). This is very surprising, but is actually the intended functionality:
|
|
32
|
-
|
|
33
|
-
* https://github.com/fastapi/sqlmodel/discussions/897
|
|
34
|
-
* https://github.com/fastapi/sqlmodel/pull/1041
|
|
35
|
-
* https://github.com/fastapi/sqlmodel/issues/453
|
|
36
|
-
* https://github.com/fastapi/sqlmodel/issues/52#issuecomment-1311987732
|
|
37
|
-
|
|
38
|
-
For validation:
|
|
39
|
-
|
|
40
|
-
* When consuming API data, use a separate shadow model to validate the data with `table=False` and then inherit from that model in a model with `table=True`.
|
|
41
|
-
* When validating ORM data, use SQL Alchemy hooks.
|
|
42
|
-
|
|
43
|
-
<!--
|
|
44
|
-
|
|
45
|
-
This looks neat
|
|
46
|
-
https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L155
|
|
47
|
-
schema_extra={
|
|
48
|
-
'pattern': r'^[a-z0-9_\-\.]+\@[a-z0-9_\-\.]+\.[a-z\.]+$'
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
extra constraints
|
|
52
|
-
|
|
53
|
-
https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L424C1-L426C6
|
|
54
|
-
-->
|
|
55
|
-
## Related Projects
|
|
56
|
-
|
|
57
|
-
* https://github.com/woofz/sqlmodel-basecrud
|
|
58
|
-
|
|
59
|
-
## Inspiration
|
|
60
|
-
|
|
61
|
-
* https://github.com/peterdresslar/fastapi-sqlmodel-alembic-pg
|
|
62
|
-
* [Albemic instructions](https://github.com/fastapi/sqlmodel/pull/899/files)
|
|
63
|
-
* https://github.com/fastapiutils/fastapi-utils/
|
|
64
|
-
* https://github.com/fastapi/full-stack-fastapi-template
|
|
65
|
-
* https://github.com/DarylStark/my_data/
|
|
66
|
-
* https://github.com/petrgazarov/FastAPI-app/tree/main/fastapi_app
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
activemodel/__init__.py,sha256=lO75TeJeSm7spmP4P1_Z5oIXBHlL7kKjr1LS2Z4dNUA,109
|
|
2
|
-
activemodel/_session_manager.py,sha256=ojFIpK_fl1-JZsj98zn-zp4RIK1_eyDl-Ga4F1XXbVw,5249
|
|
3
|
-
activemodel/base_model.py,sha256=y8gLmowqk950AELE67u69YJAT9pJSmm67apjOd1g2yo,8081
|
|
4
|
-
activemodel/logger.py,sha256=vU7QiGSy_AJuJFmClUocqIJ-Ltku_8C24ZU8L6fLJR0,53
|
|
5
|
-
activemodel/query_wrapper.py,sha256=zqaA3yAHPTfGLrkzZ6ge6byedV497C7YG1QJe_4ah7Q,2100
|
|
6
|
-
activemodel/session_manager.py,sha256=zU4Eu3YeKSlZrYEBkN70XAHJOHka40Rl4qMBZvEgNwA,1822
|
|
7
|
-
activemodel/utils.py,sha256=dY6wvAS2RCqMVMQfnpXGJxgWRBet0jV5ZXxGIRX1iRw,476
|
|
8
|
-
activemodel/mixins/__init__.py,sha256=VAVLc96oSSultP0BNAlaE3ZBNGlcuVD0JjS1FMCno7k,72
|
|
9
|
-
activemodel/mixins/timestamps.py,sha256=Q-IFljeVVJQqw3XHdOi7dkqzefiVg1zhJvq_bldpmjg,992
|
|
10
|
-
activemodel/mixins/typeid.py,sha256=0TOJ74As9JL4WVvlRm0yuZB49xHAcX0n1fzwKpgdpys,894
|
|
11
|
-
activemodel/pytest/__init__.py,sha256=W9KKQHbPkyq0jrMXaiL8hG2Nsbjy_LN9HhvgGm8W_7g,98
|
|
12
|
-
activemodel/pytest/transaction.py,sha256=rrsoHnbu79kNdnI5fZeOZr5hzrLB-cQH10MueQp5jV4,1670
|
|
13
|
-
activemodel/pytest/truncate.py,sha256=BdltCtLQNPDgRSxpBnGYGSjB_7DAceV5kHdQ_vLrw74,1583
|
|
14
|
-
activemodel/types/typeid.py,sha256=rcr9tSiu5rowD_WOcF4zzBpEUy2izmYEPteDQgCIbhs,1799
|
|
15
|
-
activemodel-0.5.0.dist-info/LICENSE,sha256=L8mmpX47rB-xtJ_HsK0zpfO6viEjxbLYGn70BMp8os4,1071
|
|
16
|
-
activemodel-0.5.0.dist-info/METADATA,sha256=4nieriFioXBxTPOIuxul3RXzNMwKPiDgSYX4KMkwSbQ,2429
|
|
17
|
-
activemodel-0.5.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
18
|
-
activemodel-0.5.0.dist-info/entry_points.txt,sha256=YLX62TP_hR-n3HMBkdBex4W7XRiyOtIPkwy22puIjjQ,61
|
|
19
|
-
activemodel-0.5.0.dist-info/top_level.txt,sha256=JCMUN_seFIi6GXtnTQRWfxXDx6Oj1uok8qapQWbWKDM,12
|
|
20
|
-
activemodel-0.5.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
activemodel
|
|
File without changes
|
|
File without changes
|