activemodel 0.10.0__tar.gz → 0.11.0__tar.gz
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-0.10.0 → activemodel-0.11.0}/CHANGELOG.md +20 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/PKG-INFO +14 -11
- {activemodel-0.10.0 → activemodel-0.11.0}/README.md +13 -10
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/base_model.py +2 -1
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/mixins/typeid.py +9 -1
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/session_manager.py +20 -4
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/types/typeid.py +35 -22
- activemodel-0.11.0/activemodel/types/typeid_patch.py +22 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/pyproject.toml +1 -1
- activemodel-0.11.0/test/types/typeid_mixin_test.py +22 -0
- activemodel-0.11.0/test/types/typeid_pydantic_test.py +48 -0
- activemodel-0.10.0/test/typeid_test.py → activemodel-0.11.0/test/types/typeid_sqlmodel_test.py +0 -19
- {activemodel-0.10.0 → activemodel-0.11.0}/.envrc +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/.github/dependabot.yml +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/.github/workflows/build_and_publish.yml +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/.github/workflows/repo-sync.yml +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/.gitignore +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/.tool-versions +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/.vscode/settings.json +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/Justfile +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/LICENSE +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/Makefile +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/TODO +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/celery.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/errors.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/get_column_from_field_patch.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/logger.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/mixins/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/mixins/pydantic_json.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/mixins/soft_delete.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/mixins/timestamps.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/pytest/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/pytest/transaction.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/pytest/truncate.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/query_wrapper.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/types/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/activemodel/utils.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/docker-compose.yml +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground/alternative_typeid_mixin.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground/comments.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground/env-with-model.patch +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground/extract_comments.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground/field.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground/middleware.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground/old_session_manager.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground/pydantic_validation.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/playground.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/comments_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/conftest.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/delete_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/fastapi_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/import_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/migrations/README +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/migrations/alembic.ini +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/migrations/env.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/migrations/script.py.mako +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/migrations_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/models.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/mutation_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/nested_pydantic_json_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/orm/test_upsert.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/orm_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/session_manager_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/table_name_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/test_wrapper.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/test/utils.py +0 -0
- {activemodel-0.10.0 → activemodel-0.11.0}/uv.lock +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0](https://github.com/iloveitaly/activemodel/compare/v0.10.0...v0.11.0) (2025-04-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add pydantic patch for TypeID schema serialization ([767726e](https://github.com/iloveitaly/activemodel/commit/767726eaf2a20f1c65162ba7d3a599495c83f721))
|
|
9
|
+
* add typeid prefix to db field comments ([34e4574](https://github.com/iloveitaly/activemodel/commit/34e457488a6e59c4e7daf1d7495b59ac64bd7ea2))
|
|
10
|
+
* render TypeIDType in plain old pydantic models ([8aa0a4d](https://github.com/iloveitaly/activemodel/commit/8aa0a4db51b425a60889064e723129041e418da5))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* typing on upsert properly returns self ([8965113](https://github.com/iloveitaly/activemodel/commit/896511399c818e8a4a1c01ed324921e04073a279))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* add advanced SQLAlchemy tips to README.md ([c624ac0](https://github.com/iloveitaly/activemodel/commit/c624ac0712598d06759166ce9f91a37f40fefcfe))
|
|
21
|
+
* update comments in session_manager to clarify usage ([e657a2e](https://github.com/iloveitaly/activemodel/commit/e657a2e5985be6f0770bf945fc062a05dcff8cc8))
|
|
22
|
+
|
|
3
23
|
## [0.10.0](https://github.com/iloveitaly/activemodel/compare/v0.9.0...v0.10.0) (2025-04-01)
|
|
4
24
|
|
|
5
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: activemodel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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>
|
|
@@ -110,14 +110,14 @@ index 0d07420..a63631c 100644
|
|
|
110
110
|
# Use forward slashes (/) also on windows to provide an os agnostic path
|
|
111
111
|
-script_location = .
|
|
112
112
|
+script_location = migrations
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
115
115
|
# Uncomment the line below if you want the files to be prepended with date and time
|
|
116
116
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
|
117
117
|
# for all available tokens
|
|
118
118
|
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
119
119
|
+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
# sys.path path, will be prepended to sys.path if present.
|
|
122
122
|
# defaults to the current working directory.
|
|
123
123
|
diff --git i/test/migrations/env.py w/test/migrations/env.py
|
|
@@ -129,12 +129,12 @@ index 36112a3..a1e15c2 100644
|
|
|
129
129
|
+# isort: off
|
|
130
130
|
+
|
|
131
131
|
from logging.config import fileConfig
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
from sqlalchemy import engine_from_config
|
|
134
134
|
@@ -14,11 +17,17 @@ config = context.config
|
|
135
135
|
if config.config_file_name is not None:
|
|
136
136
|
fileConfig(config.config_file_name)
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
+from sqlmodel import SQLModel
|
|
139
139
|
+from test.models import *
|
|
140
140
|
+from test.utils import database_url
|
|
@@ -147,7 +147,7 @@ index 36112a3..a1e15c2 100644
|
|
|
147
147
|
# target_metadata = mymodel.Base.metadata
|
|
148
148
|
-target_metadata = None
|
|
149
149
|
+target_metadata = SQLModel.metadata
|
|
150
|
-
|
|
150
|
+
|
|
151
151
|
# other values from the config, defined by the needs of env.py,
|
|
152
152
|
# can be acquired:
|
|
153
153
|
diff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako
|
|
@@ -155,13 +155,13 @@ index fbc4b07..9dc78bb 100644
|
|
|
155
155
|
--- i/test/migrations/script.py.mako
|
|
156
156
|
+++ w/test/migrations/script.py.mako
|
|
157
157
|
@@ -9,6 +9,8 @@ from typing import Sequence, Union
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
from alembic import op
|
|
160
160
|
import sqlalchemy as sa
|
|
161
161
|
+import sqlmodel
|
|
162
162
|
+import activemodel
|
|
163
163
|
${imports if imports else ""}
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
# revision identifiers, used by Alembic.
|
|
166
166
|
```
|
|
167
167
|
|
|
@@ -178,7 +178,7 @@ This tool is added to all `BaseModel`s and makes it easy to write SQL queries. S
|
|
|
178
178
|
|
|
179
179
|
### Easy Database Sessions
|
|
180
180
|
|
|
181
|
-
I hate the idea f
|
|
181
|
+
I hate the idea f
|
|
182
182
|
|
|
183
183
|
* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.
|
|
184
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.
|
|
@@ -186,7 +186,7 @@ I hate the idea f
|
|
|
186
186
|
There are a couple of thorny problems we need to solve for here:
|
|
187
187
|
|
|
188
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
|
-
*
|
|
189
|
+
*
|
|
190
190
|
|
|
191
191
|
https://github.com/tomwojcik/starlette-context
|
|
192
192
|
|
|
@@ -202,8 +202,11 @@ https://github.com/tomwojcik/starlette-context
|
|
|
202
202
|
SQLModel & SQLAlchemy are tricky. Here are some useful internal tricks:
|
|
203
203
|
|
|
204
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`
|
|
205
|
+
* `ModelClass.relationship_name.property.local_columns`
|
|
206
206
|
* Get cached fields from a model `object_state(instance).dict.get(field_name)`
|
|
207
|
+
* Set the value on a field, without marking it as dirty `attributes.set_committed_value(instance, field_name, val)`
|
|
208
|
+
* Is a model dirty `instance_state(instance).modified`
|
|
209
|
+
* `select(Table).outerjoin??` won't work in a ipython session, but `Table.__table__.outerjoin??` will. `__table__` is a reference to the underlying SQLAlchemy table record.
|
|
207
210
|
|
|
208
211
|
### TypeID
|
|
209
212
|
|
|
@@ -95,14 +95,14 @@ index 0d07420..a63631c 100644
|
|
|
95
95
|
# Use forward slashes (/) also on windows to provide an os agnostic path
|
|
96
96
|
-script_location = .
|
|
97
97
|
+script_location = migrations
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
100
100
|
# Uncomment the line below if you want the files to be prepended with date and time
|
|
101
101
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
|
102
102
|
# for all available tokens
|
|
103
103
|
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
104
104
|
+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
# sys.path path, will be prepended to sys.path if present.
|
|
107
107
|
# defaults to the current working directory.
|
|
108
108
|
diff --git i/test/migrations/env.py w/test/migrations/env.py
|
|
@@ -114,12 +114,12 @@ index 36112a3..a1e15c2 100644
|
|
|
114
114
|
+# isort: off
|
|
115
115
|
+
|
|
116
116
|
from logging.config import fileConfig
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
from sqlalchemy import engine_from_config
|
|
119
119
|
@@ -14,11 +17,17 @@ config = context.config
|
|
120
120
|
if config.config_file_name is not None:
|
|
121
121
|
fileConfig(config.config_file_name)
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
+from sqlmodel import SQLModel
|
|
124
124
|
+from test.models import *
|
|
125
125
|
+from test.utils import database_url
|
|
@@ -132,7 +132,7 @@ index 36112a3..a1e15c2 100644
|
|
|
132
132
|
# target_metadata = mymodel.Base.metadata
|
|
133
133
|
-target_metadata = None
|
|
134
134
|
+target_metadata = SQLModel.metadata
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
# other values from the config, defined by the needs of env.py,
|
|
137
137
|
# can be acquired:
|
|
138
138
|
diff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako
|
|
@@ -140,13 +140,13 @@ index fbc4b07..9dc78bb 100644
|
|
|
140
140
|
--- i/test/migrations/script.py.mako
|
|
141
141
|
+++ w/test/migrations/script.py.mako
|
|
142
142
|
@@ -9,6 +9,8 @@ from typing import Sequence, Union
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
from alembic import op
|
|
145
145
|
import sqlalchemy as sa
|
|
146
146
|
+import sqlmodel
|
|
147
147
|
+import activemodel
|
|
148
148
|
${imports if imports else ""}
|
|
149
|
-
|
|
149
|
+
|
|
150
150
|
# revision identifiers, used by Alembic.
|
|
151
151
|
```
|
|
152
152
|
|
|
@@ -163,7 +163,7 @@ This tool is added to all `BaseModel`s and makes it easy to write SQL queries. S
|
|
|
163
163
|
|
|
164
164
|
### Easy Database Sessions
|
|
165
165
|
|
|
166
|
-
I hate the idea f
|
|
166
|
+
I hate the idea f
|
|
167
167
|
|
|
168
168
|
* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.
|
|
169
169
|
* 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.
|
|
@@ -171,7 +171,7 @@ I hate the idea f
|
|
|
171
171
|
There are a couple of thorny problems we need to solve for here:
|
|
172
172
|
|
|
173
173
|
* 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.
|
|
174
|
-
*
|
|
174
|
+
*
|
|
175
175
|
|
|
176
176
|
https://github.com/tomwojcik/starlette-context
|
|
177
177
|
|
|
@@ -187,8 +187,11 @@ https://github.com/tomwojcik/starlette-context
|
|
|
187
187
|
SQLModel & SQLAlchemy are tricky. Here are some useful internal tricks:
|
|
188
188
|
|
|
189
189
|
* `__sqlmodel_relationships__` is where any `RelationshipInfo` objects are stored. This is used to generate relationship fields on the object.
|
|
190
|
-
* `ModelClass.relationship_name.property.local_columns`
|
|
190
|
+
* `ModelClass.relationship_name.property.local_columns`
|
|
191
191
|
* Get cached fields from a model `object_state(instance).dict.get(field_name)`
|
|
192
|
+
* Set the value on a field, without marking it as dirty `attributes.set_committed_value(instance, field_name, val)`
|
|
193
|
+
* Is a model dirty `instance_state(instance).modified`
|
|
194
|
+
* `select(Table).outerjoin??` won't work in a ipython session, but `Table.__table__.outerjoin??` will. `__table__` is a reference to the underlying SQLAlchemy table record.
|
|
192
195
|
|
|
193
196
|
### TypeID
|
|
194
197
|
|
|
@@ -196,12 +196,13 @@ class BaseModel(SQLModel):
|
|
|
196
196
|
"convenience method to avoid having to write .select().where() in order to add conditions"
|
|
197
197
|
return cls.select().where(*args)
|
|
198
198
|
|
|
199
|
+
# TODO we should add an instance method for this as well
|
|
199
200
|
@classmethod
|
|
200
201
|
def upsert(
|
|
201
202
|
cls,
|
|
202
203
|
data: dict[str, t.Any],
|
|
203
204
|
unique_by: str | list[str],
|
|
204
|
-
) ->
|
|
205
|
+
) -> t.Self:
|
|
205
206
|
"""
|
|
206
207
|
This method will insert a new record if it doesn't exist, or update the existing record if it does.
|
|
207
208
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from sqlmodel import Column, Field
|
|
2
2
|
from typeid import TypeID
|
|
3
3
|
|
|
4
|
+
from activemodel.types import typeid_patch # noqa: F401
|
|
4
5
|
from activemodel.types.typeid import TypeIDType
|
|
5
6
|
|
|
6
7
|
# global list of prefixes to ensure uniqueness
|
|
@@ -8,6 +9,10 @@ _prefixes: list[str] = []
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def TypeIDMixin(prefix: str):
|
|
12
|
+
"""
|
|
13
|
+
Mixin that adds a TypeID primary key field to a SQLModel. Specify the prefix to use for the TypeID.
|
|
14
|
+
"""
|
|
15
|
+
|
|
11
16
|
# make sure duplicate prefixes are not used!
|
|
12
17
|
# NOTE this will cause issues on code reloads
|
|
13
18
|
assert prefix
|
|
@@ -23,9 +28,12 @@ def TypeIDMixin(prefix: str):
|
|
|
23
28
|
TypeIDType(prefix),
|
|
24
29
|
primary_key=True,
|
|
25
30
|
nullable=False,
|
|
31
|
+
# default on the sa_column level ensures that an ID is generated when creating a new record, even when
|
|
32
|
+
# raw SQLAlchemy operations are used instead of activemodel operations
|
|
26
33
|
default=lambda: TypeID(prefix),
|
|
27
34
|
),
|
|
28
|
-
#
|
|
35
|
+
# add a database comment to document the prefix, since it's not stored in the DB otherwise
|
|
36
|
+
description=f"TypeID with prefix: {prefix}",
|
|
29
37
|
)
|
|
30
38
|
|
|
31
39
|
_prefixes.append(prefix)
|
|
@@ -47,7 +47,7 @@ class SessionManager:
|
|
|
47
47
|
"singleton instance of SessionManager"
|
|
48
48
|
|
|
49
49
|
session_connection: Connection | None
|
|
50
|
-
"optionally specify a specific session connection to use for all get_session() calls, useful for testing"
|
|
50
|
+
"optionally specify a specific session connection to use for all get_session() calls, useful for testing and migrations"
|
|
51
51
|
|
|
52
52
|
@classmethod
|
|
53
53
|
def get_instance(cls, database_url: str | None = None) -> "SessionManager":
|
|
@@ -82,6 +82,8 @@ class SessionManager:
|
|
|
82
82
|
return self._engine
|
|
83
83
|
|
|
84
84
|
def get_session(self):
|
|
85
|
+
"get a new database session, respecting any globally set sessions"
|
|
86
|
+
|
|
85
87
|
if gsession := _session_context.get():
|
|
86
88
|
|
|
87
89
|
@contextlib.contextmanager
|
|
@@ -103,23 +105,37 @@ def init(database_url: str):
|
|
|
103
105
|
|
|
104
106
|
|
|
105
107
|
def get_engine():
|
|
108
|
+
"alias to get the database engine without importing SessionManager"
|
|
106
109
|
return SessionManager.get_instance().get_engine()
|
|
107
110
|
|
|
108
111
|
|
|
109
112
|
def get_session():
|
|
113
|
+
"alias to get a database session without importing SessionManager"
|
|
110
114
|
return SessionManager.get_instance().get_session()
|
|
111
115
|
|
|
112
116
|
|
|
113
|
-
# contextvars must be at the top-level of a module! You will not get a warning if you don't do this.
|
|
114
|
-
# ContextVar is implemented in C, so it's very special and is both thread-safe and asyncio safe. This variable gives us
|
|
115
|
-
# a place to persist a session to use globally across the application.
|
|
116
117
|
_session_context = contextvars.ContextVar[Session | None](
|
|
117
118
|
"session_context", default=None
|
|
118
119
|
)
|
|
120
|
+
"""
|
|
121
|
+
This is a VERY important ContextVar, it sets a global session to be used across all ActiveModel operations by default
|
|
122
|
+
and ensures get_session() uses this session as well.
|
|
123
|
+
|
|
124
|
+
contextvars must be at the top-level of a module! You will not get a warning if you don't do this.
|
|
125
|
+
ContextVar is implemented in C, so it's very special and is both thread-safe and asyncio safe. This variable gives us
|
|
126
|
+
a place to persist a session to use globally across the application.
|
|
127
|
+
"""
|
|
119
128
|
|
|
120
129
|
|
|
121
130
|
@contextlib.contextmanager
|
|
122
131
|
def global_session():
|
|
132
|
+
"""
|
|
133
|
+
Generate a session shared across all activemodel calls.
|
|
134
|
+
|
|
135
|
+
Alternatively, you can pass a session to use globally into the context manager, which is helpful for migrations
|
|
136
|
+
and testing.
|
|
137
|
+
"""
|
|
138
|
+
|
|
123
139
|
if _session_context.get() is not None:
|
|
124
140
|
raise RuntimeError("global session already set")
|
|
125
141
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Lifted from: https://github.com/akhundMurad/typeid-python/blob/main/examples/sqlalchemy.py
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Optional
|
|
6
5
|
from uuid import UUID
|
|
7
6
|
|
|
8
7
|
from pydantic import (
|
|
@@ -19,25 +18,32 @@ from activemodel.errors import TypeIDValidationError
|
|
|
19
18
|
class TypeIDType(types.TypeDecorator):
|
|
20
19
|
"""
|
|
21
20
|
A SQLAlchemy TypeDecorator that allows storing TypeIDs in the database.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
|
|
22
|
+
The prefix will not be persisted to the database, instead the database-native UUID field will be used.
|
|
23
|
+
At retrieval time a TypeID will be constructed (in python) based on the configured prefix and the UUID
|
|
24
|
+
value from the database.
|
|
25
|
+
|
|
26
|
+
For example:
|
|
27
|
+
|
|
28
|
+
>>> id = mapped_column(
|
|
29
|
+
>>> TypeIDType("user"),
|
|
30
|
+
>>> primary_key=True,
|
|
31
|
+
>>> default=lambda: TypeID("user")
|
|
32
|
+
>>> )
|
|
33
|
+
|
|
34
|
+
Will result in TypeIDs such as "user_01h45ytscbebyvny4gc8cr8ma2". There's a mixin provided to make it easy
|
|
35
|
+
to add a `id` pk field to your model with a specific prefix.
|
|
33
36
|
"""
|
|
34
37
|
|
|
38
|
+
# TODO are we sure we wouldn't use TypeID here?
|
|
35
39
|
impl = types.Uuid
|
|
40
|
+
# TODO why the types version?
|
|
36
41
|
# impl = uuid.UUID
|
|
42
|
+
|
|
37
43
|
cache_ok = True
|
|
38
|
-
prefix:
|
|
44
|
+
prefix: str
|
|
39
45
|
|
|
40
|
-
def __init__(self, prefix:
|
|
46
|
+
def __init__(self, prefix: str, *args, **kwargs):
|
|
41
47
|
self.prefix = prefix
|
|
42
48
|
super().__init__(*args, **kwargs)
|
|
43
49
|
|
|
@@ -90,6 +96,8 @@ class TypeIDType(types.TypeDecorator):
|
|
|
90
96
|
raise ValueError("Unexpected input type")
|
|
91
97
|
|
|
92
98
|
def process_result_value(self, value, dialect):
|
|
99
|
+
"convert a raw UUID, without a prefix, to a TypeID with the correct prefix"
|
|
100
|
+
|
|
93
101
|
if value is None:
|
|
94
102
|
return None
|
|
95
103
|
|
|
@@ -123,13 +131,19 @@ class TypeIDType(types.TypeDecorator):
|
|
|
123
131
|
- https://github.com/alice-biometrics/petisco/blob/b01ef1b84949d156f73919e126ed77aa8e0b48dd/petisco/base/domain/model/uuid.py#L50
|
|
124
132
|
"""
|
|
125
133
|
|
|
134
|
+
def convert_from_string(value: str | TypeID) -> TypeID:
|
|
135
|
+
if isinstance(value, TypeID):
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
return TypeID.from_string(value)
|
|
139
|
+
|
|
126
140
|
from_uuid_schema = core_schema.chain_schema(
|
|
127
141
|
[
|
|
128
142
|
# TODO not sure how this is different from the UUID schema, should try it out.
|
|
129
143
|
# core_schema.is_instance_schema(TypeID),
|
|
130
144
|
# core_schema.uuid_schema(),
|
|
131
145
|
core_schema.no_info_plain_validator_function(
|
|
132
|
-
|
|
146
|
+
convert_from_string,
|
|
133
147
|
json_schema_input_schema=core_schema.str_schema(),
|
|
134
148
|
),
|
|
135
149
|
]
|
|
@@ -151,11 +165,10 @@ class TypeIDType(types.TypeDecorator):
|
|
|
151
165
|
# )
|
|
152
166
|
# },
|
|
153
167
|
python_schema=core_schema.union_schema([from_uuid_schema]),
|
|
154
|
-
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
155
|
-
lambda x: str(x)
|
|
156
|
-
),
|
|
168
|
+
serialization=core_schema.plain_serializer_function_ser_schema(str),
|
|
157
169
|
)
|
|
158
170
|
|
|
171
|
+
# TODO I have a feeling that the `serialization` param in the above method solves this for us.
|
|
159
172
|
@classmethod
|
|
160
173
|
def __get_pydantic_json_schema__(
|
|
161
174
|
cls, schema: CoreSchema, handler: GetJsonSchemaHandler
|
|
@@ -164,18 +177,18 @@ class TypeIDType(types.TypeDecorator):
|
|
|
164
177
|
Called when generating the openapi schema. This overrides the `function-plain` type which
|
|
165
178
|
is generated by the `no_info_plain_validator_function`.
|
|
166
179
|
|
|
167
|
-
This
|
|
180
|
+
This logic seems to be a hot part of the codebase, so I'd expect this to break as pydantic
|
|
168
181
|
fastapi continue to evolve.
|
|
169
182
|
|
|
170
183
|
Note that this method can return multiple types. A return value can be as simple as:
|
|
171
184
|
|
|
172
|
-
{"type": "string"}
|
|
185
|
+
>>> {"type": "string"}
|
|
173
186
|
|
|
174
187
|
Or, you could return a more specific JSON schema type:
|
|
175
188
|
|
|
176
|
-
core_schema.uuid_schema()
|
|
189
|
+
>>> core_schema.uuid_schema()
|
|
177
190
|
|
|
178
|
-
The problem with using something like uuid_schema is the
|
|
191
|
+
The problem with using something like uuid_schema is the specific patterns
|
|
179
192
|
|
|
180
193
|
https://github.com/BeanieODM/beanie/blob/2190cd9d1fc047af477d5e6897cc283799f54064/beanie/odm/fields.py#L153
|
|
181
194
|
"""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any, Type
|
|
2
|
+
|
|
3
|
+
from pydantic import GetCoreSchemaHandler
|
|
4
|
+
from pydantic_core import CoreSchema, core_schema
|
|
5
|
+
|
|
6
|
+
from typeid import TypeID
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def get_pydantic_core_schema(
|
|
11
|
+
cls: Type[TypeID], source_type: Any, handler: GetCoreSchemaHandler
|
|
12
|
+
) -> CoreSchema:
|
|
13
|
+
return core_schema.union_schema(
|
|
14
|
+
[
|
|
15
|
+
core_schema.str_schema(),
|
|
16
|
+
core_schema.is_instance_schema(cls),
|
|
17
|
+
],
|
|
18
|
+
serialization=core_schema.plain_serializer_function_ser_schema(str),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
TypeID.__get_pydantic_core_schema__ = get_pydantic_core_schema
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
5
|
+
from typeid import TypeID
|
|
6
|
+
|
|
7
|
+
from test.models import TYPEID_PREFIX, ExampleWithId
|
|
8
|
+
from test.utils import temporary_tables
|
|
9
|
+
|
|
10
|
+
from activemodel.mixins import TypeIDMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_enforces_unique_prefixes():
|
|
14
|
+
TypeIDMixin("hi")
|
|
15
|
+
|
|
16
|
+
with pytest.raises(AssertionError):
|
|
17
|
+
TypeIDMixin("hi")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_no_empty_prefixes_test():
|
|
21
|
+
with pytest.raises(AssertionError):
|
|
22
|
+
TypeIDMixin("")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from activemodel.types.typeid import TypeIDType
|
|
3
|
+
from test.models import ExampleWithId
|
|
4
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
5
|
+
from typeid import TypeID
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_json_schema(create_and_wipe_database):
|
|
9
|
+
"json schema generation shouldn't be meaningfully different than json rendering, but let's check it anyway"
|
|
10
|
+
|
|
11
|
+
example = ExampleWithId().save()
|
|
12
|
+
example.model_json_schema()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PydanticResponseModel(PydanticBaseModel):
|
|
16
|
+
id: TypeID
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_typeid_render(create_and_wipe_database):
|
|
20
|
+
"""
|
|
21
|
+
ensure that pydantic models can render the type id, this requires dunder methods to be added to the TypeID type
|
|
22
|
+
which is done thruogh the typeid_patch file
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
example = ExampleWithId().save()
|
|
26
|
+
response = PydanticResponseModel(id=example.id)
|
|
27
|
+
|
|
28
|
+
# check that the TypeID is serialized as a string
|
|
29
|
+
assert json.loads(response.model_dump_json())["id"] == str(example.id)
|
|
30
|
+
assert response.model_json_schema()["properties"]["id"]["type"] == "string"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PydanticResponseTypeIDTypeModel(PydanticBaseModel):
|
|
34
|
+
id: TypeIDType
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_typeid_type_render(create_and_wipe_database):
|
|
38
|
+
"""
|
|
39
|
+
ensure that pydantic models can render the type id, this requires dunder methods to be added to the TypeID type
|
|
40
|
+
which is done thruogh the typeid_patch file
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
example = ExampleWithId().save()
|
|
44
|
+
response = PydanticResponseTypeIDTypeModel(id=example.id)
|
|
45
|
+
|
|
46
|
+
# check that the TypeID is serialized as a string
|
|
47
|
+
assert json.loads(response.model_dump_json())["id"] == str(example.id)
|
|
48
|
+
assert response.model_json_schema()["properties"]["id"]["type"] == "string"
|
activemodel-0.10.0/test/typeid_test.py → activemodel-0.11.0/test/types/typeid_sqlmodel_test.py
RENAMED
|
@@ -10,18 +10,6 @@ from test.utils import temporary_tables
|
|
|
10
10
|
from activemodel.mixins import TypeIDMixin
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def test_enforces_unique_prefixes():
|
|
14
|
-
TypeIDMixin("hi")
|
|
15
|
-
|
|
16
|
-
with pytest.raises(AssertionError):
|
|
17
|
-
TypeIDMixin("hi")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_no_empty_prefixes_test():
|
|
21
|
-
with pytest.raises(AssertionError):
|
|
22
|
-
TypeIDMixin("")
|
|
23
|
-
|
|
24
|
-
|
|
25
13
|
def test_get_through_prefixed_uid():
|
|
26
14
|
type_uid = TypeID(prefix=TYPEID_PREFIX)
|
|
27
15
|
|
|
@@ -84,10 +72,3 @@ def test_render_typeid(create_and_wipe_database):
|
|
|
84
72
|
assert json.loads(wrapped_example.model_dump_json())["example"]["id"] == str(
|
|
85
73
|
example.id
|
|
86
74
|
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def test_json_schema(create_and_wipe_database):
|
|
90
|
-
"json schema generation shouldn't be meaningfully different than json rendering, but let's check it anyway"
|
|
91
|
-
|
|
92
|
-
example = ExampleWithId().save()
|
|
93
|
-
example.model_json_schema()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|