activemodel 0.10.0__tar.gz → 0.12.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.12.0}/.envrc +1 -1
- {activemodel-0.10.0 → activemodel-0.12.0}/.github/workflows/build_and_publish.yml +5 -4
- {activemodel-0.10.0 → activemodel-0.12.0}/.github/workflows/repo-sync.yml +1 -1
- activemodel-0.12.0/.tool-versions +3 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/CHANGELOG.md +58 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/Makefile +4 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/PKG-INFO +40 -16
- {activemodel-0.10.0 → activemodel-0.12.0}/README.md +38 -14
- {activemodel-0.10.0 → activemodel-0.12.0}/TODO +15 -1
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/base_model.py +25 -9
- activemodel-0.12.0/activemodel/cli/__init__.py +147 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/mixins/timestamps.py +4 -1
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/mixins/typeid.py +10 -2
- activemodel-0.12.0/activemodel/pytest/__init__.py +2 -0
- activemodel-0.12.0/activemodel/pytest/factories.py +102 -0
- activemodel-0.12.0/activemodel/pytest/plugin.py +81 -0
- activemodel-0.12.0/activemodel/pytest/transaction.py +150 -0
- activemodel-0.12.0/activemodel/pytest/truncate.py +147 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/query_wrapper.py +12 -2
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/session_manager.py +64 -14
- activemodel-0.12.0/activemodel/types/sqlalchemy_protocol.py +10 -0
- activemodel-0.12.0/activemodel/types/sqlalchemy_protocol.pyi +132 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/types/typeid.py +36 -22
- activemodel-0.12.0/activemodel/types/typeid_patch.py +22 -0
- activemodel-0.12.0/activemodel/utils.py +29 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/pyproject.toml +11 -4
- {activemodel-0.10.0 → activemodel-0.12.0}/test/conftest.py +7 -3
- activemodel-0.12.0/test/factory_test.py +57 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/orm_test.py +27 -0
- activemodel-0.12.0/test/pytest/pytest_test.py +134 -0
- activemodel-0.12.0/test/session_manager_test.py +60 -0
- activemodel-0.12.0/test/table_name_test.py +15 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/test_wrapper.py +17 -0
- activemodel-0.12.0/test/types/typeid_mixin_test.py +22 -0
- activemodel-0.12.0/test/types/typeid_pydantic_test.py +48 -0
- activemodel-0.10.0/test/typeid_test.py → activemodel-0.12.0/test/types/typeid_sqlmodel_test.py +0 -19
- activemodel-0.12.0/uv.lock +1644 -0
- activemodel-0.10.0/.tool-versions +0 -3
- activemodel-0.10.0/activemodel/pytest/__init__.py +0 -2
- activemodel-0.10.0/activemodel/pytest/transaction.py +0 -63
- activemodel-0.10.0/activemodel/pytest/truncate.py +0 -46
- activemodel-0.10.0/activemodel/utils.py +0 -65
- activemodel-0.10.0/test/session_manager_test.py +0 -22
- activemodel-0.10.0/test/table_name_test.py +0 -14
- activemodel-0.10.0/uv.lock +0 -1367
- {activemodel-0.10.0 → activemodel-0.12.0}/.github/dependabot.yml +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/.gitignore +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/.vscode/settings.json +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/Justfile +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/LICENSE +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/celery.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/errors.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/get_column_from_field_patch.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/logger.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/mixins/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/mixins/pydantic_json.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/mixins/soft_delete.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/activemodel/types/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/docker-compose.yml +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground/alternative_typeid_mixin.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground/comments.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground/env-with-model.patch +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground/extract_comments.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground/field.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground/middleware.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground/old_session_manager.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground/pydantic_validation.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/playground.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/__init__.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/comments_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/delete_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/fastapi_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/import_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/migrations/README +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/migrations/alembic.ini +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/migrations/env.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/migrations/script.py.mako +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/migrations_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/models.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/mutation_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/nested_pydantic_json_test.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/orm/test_upsert.py +0 -0
- {activemodel-0.10.0 → activemodel-0.12.0}/test/utils.py +0 -0
|
@@ -4,6 +4,7 @@ on:
|
|
|
4
4
|
branches:
|
|
5
5
|
- main
|
|
6
6
|
- master
|
|
7
|
+
pull_request:
|
|
7
8
|
|
|
8
9
|
env:
|
|
9
10
|
# avoid build failures due to flaky pypi
|
|
@@ -34,8 +35,8 @@ jobs:
|
|
|
34
35
|
needs: [release-please]
|
|
35
36
|
if: needs.release-please.outputs.release_created
|
|
36
37
|
steps:
|
|
37
|
-
- uses: actions/checkout@
|
|
38
|
-
- uses: jdx/mise-action@
|
|
38
|
+
- uses: actions/checkout@v5
|
|
39
|
+
- uses: jdx/mise-action@v3
|
|
39
40
|
- run: direnv allow . && direnv export gha >> "$GITHUB_ENV"
|
|
40
41
|
- run: uv build
|
|
41
42
|
- run: uv publish --token ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -49,8 +50,8 @@ jobs:
|
|
|
49
50
|
python-version: ["3.13", "3.12"]
|
|
50
51
|
runs-on: ${{ matrix.os }}
|
|
51
52
|
steps:
|
|
52
|
-
- uses: actions/checkout@
|
|
53
|
-
- uses: jdx/mise-action@
|
|
53
|
+
- uses: actions/checkout@v5
|
|
54
|
+
- uses: jdx/mise-action@v3
|
|
54
55
|
- run: mise use python@${{ matrix.python-version }}
|
|
55
56
|
- run: docker compose up -d --wait
|
|
56
57
|
- uses: iloveitaly/github-action-direnv-load-and-mask@master
|
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.0](https://github.com/iloveitaly/activemodel/compare/v0.11.0...v0.12.0) (2025-09-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add ActiveModelFactory helpers for session management and typeid generation ([a6b9915](https://github.com/iloveitaly/activemodel/commit/a6b9915cc0c74c3b0d1421bc3d0300c1a2f63426))
|
|
9
|
+
* add base SQLModel and ActiveModel polyfactory factories ([12a5e1d](https://github.com/iloveitaly/activemodel/commit/12a5e1db9be741f12db174d5a25f6c2eee61a446))
|
|
10
|
+
* add one_or_none method to BaseModel for safe queries ([7574777](https://github.com/iloveitaly/activemodel/commit/75747773f99a7a6cc82d2ca784ce607abd7ae511))
|
|
11
|
+
* add pytest db_session fixture ([22d2ad8](https://github.com/iloveitaly/activemodel/commit/22d2ad8cbd77260c1c73524f3251ef7fd145caec))
|
|
12
|
+
* add scalar method to QueryWrapper and return from delete ([3d68097](https://github.com/iloveitaly/activemodel/commit/3d680972b6a60b43de6d3826d4289b0f81847b74))
|
|
13
|
+
* add SQLAlchemy protocol generation script ([6e4524e](https://github.com/iloveitaly/activemodel/commit/6e4524ed73644bea5b837ce2a9a7a1994b19df00))
|
|
14
|
+
* add test_session context manager for test DB interactions ([d7a8112](https://github.com/iloveitaly/activemodel/commit/d7a8112f6b303a50df62ca55a0ca8a9c864e4686))
|
|
15
|
+
* export test_session from activemodel.pytest ([9bf692a](https://github.com/iloveitaly/activemodel/commit/9bf692a72937d9a87a1ba2e574cff6dc4000bafd))
|
|
16
|
+
* **pytest:** omit truncation tables, truncation db fixture ([f20f17e](https://github.com/iloveitaly/activemodel/commit/f20f17ea0854dfbde09e86f9446d57d33ab516e8))
|
|
17
|
+
* support engine options ([9b00ecd](https://github.com/iloveitaly/activemodel/commit/9b00ecd4a7b1ac95a6a16b1e7727ef37afe81e00))
|
|
18
|
+
* support passing session to global_session context manager ([567708a](https://github.com/iloveitaly/activemodel/commit/567708aeb6642e75b21932d599cd1431f62c4c65))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* add return type to ActiveModelFactory save method ([96f4a0a](https://github.com/iloveitaly/activemodel/commit/96f4a0a70eb72b14dea8df16b9e92a4b79122285))
|
|
24
|
+
* add ruff link + check on autogenerated types ([91ae5ea](https://github.com/iloveitaly/activemodel/commit/91ae5ea079533dc7e5600cf99c757ec5c6cd872f))
|
|
25
|
+
* allow reentrant global_session when using same session reference ([d4d7949](https://github.com/iloveitaly/activemodel/commit/d4d79493afdf7ef6ae36f9f6d1c2949eb2b7c393))
|
|
26
|
+
* clarify duplicate TypeID prefix error message ([a73c221](https://github.com/iloveitaly/activemodel/commit/a73c221fd51c1efa70812eb1a21a96f0af7faadc))
|
|
27
|
+
* prevent factories from setting timestamp fields by default ([5d78745](https://github.com/iloveitaly/activemodel/commit/5d7874506047b7ae91f22566e1dc2c64d68814f4))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
* added related issue ([3c47e60](https://github.com/iloveitaly/activemodel/commit/3c47e601b8f3eb8a85d9cb84a3e64df68ea10d90))
|
|
33
|
+
* clarify session management and querying in usage section ([cdadaff](https://github.com/iloveitaly/activemodel/commit/cdadafffa1ac835f2f6419f40de5bf2f281f94d2))
|
|
34
|
+
* connection pool tip ([5d92b99](https://github.com/iloveitaly/activemodel/commit/5d92b992d4f71af0bf8244826cd4d12281fe350b))
|
|
35
|
+
* fix JSONB usage in example and clarify imports in README ([386e69d](https://github.com/iloveitaly/activemodel/commit/386e69d5ad99baddfba119f62b6d3c6a53809857))
|
|
36
|
+
* improve base model tablename docstring ([de98cbc](https://github.com/iloveitaly/activemodel/commit/de98cbcff0b2ef903207f4c65fb1aa6e014cf40c))
|
|
37
|
+
* **pytest:** clarify ActiveModelFactory.save behavior with relationships and truncation ([6ee2cbe](https://github.com/iloveitaly/activemodel/commit/6ee2cbef23601943271a36a90f2f453de4519c72))
|
|
38
|
+
* **typeid:** clarify exception on invalid UUID string ([bf13a7e](https://github.com/iloveitaly/activemodel/commit/bf13a7e7e537d41a94ff926dcabf316b0d62625b))
|
|
39
|
+
* update TODOs for testing, constraints, and unique key suggestions ([6010f8f](https://github.com/iloveitaly/activemodel/commit/6010f8fec1c3bdb22d68ac3b545ebeafd6bd04ef))
|
|
40
|
+
|
|
41
|
+
## [0.11.0](https://github.com/iloveitaly/activemodel/compare/v0.10.0...v0.11.0) (2025-04-05)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Features
|
|
45
|
+
|
|
46
|
+
* add pydantic patch for TypeID schema serialization ([767726e](https://github.com/iloveitaly/activemodel/commit/767726eaf2a20f1c65162ba7d3a599495c83f721))
|
|
47
|
+
* add typeid prefix to db field comments ([34e4574](https://github.com/iloveitaly/activemodel/commit/34e457488a6e59c4e7daf1d7495b59ac64bd7ea2))
|
|
48
|
+
* render TypeIDType in plain old pydantic models ([8aa0a4d](https://github.com/iloveitaly/activemodel/commit/8aa0a4db51b425a60889064e723129041e418da5))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Bug Fixes
|
|
52
|
+
|
|
53
|
+
* typing on upsert properly returns self ([8965113](https://github.com/iloveitaly/activemodel/commit/896511399c818e8a4a1c01ed324921e04073a279))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Documentation
|
|
57
|
+
|
|
58
|
+
* add advanced SQLAlchemy tips to README.md ([c624ac0](https://github.com/iloveitaly/activemodel/commit/c624ac0712598d06759166ce9f91a37f40fefcfe))
|
|
59
|
+
* update comments in session_manager to clarify usage ([e657a2e](https://github.com/iloveitaly/activemodel/commit/e657a2e5985be6f0770bf945fc062a05dcff8cc8))
|
|
60
|
+
|
|
3
61
|
## [0.10.0](https://github.com/iloveitaly/activemodel/compare/v0.9.0...v0.10.0) (2025-04-01)
|
|
4
62
|
|
|
5
63
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: activemodel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.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>
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Keywords: activemodel,activerecord,orm,sqlalchemy,sqlmodel
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
|
-
Requires-Dist: pydash>=8.0.4
|
|
11
10
|
Requires-Dist: python-decouple-typed>=3.11.0
|
|
12
11
|
Requires-Dist: sqlmodel>=0.0.22
|
|
12
|
+
Requires-Dist: textcase>=0.4.0
|
|
13
13
|
Requires-Dist: typeid-python>=0.3.1
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
|
|
@@ -59,15 +59,17 @@ from sqlmodel import SQLModel
|
|
|
59
59
|
|
|
60
60
|
SQLModel.metadata.create_all(get_engine())
|
|
61
61
|
|
|
62
|
-
# now you can create a user!
|
|
62
|
+
# now you can create a user! without managing sessions!
|
|
63
63
|
User(a_field="a").save()
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Maybe you like JSON:
|
|
67
67
|
|
|
68
68
|
```python
|
|
69
|
-
from
|
|
69
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
70
70
|
from pydantic import BaseModel as PydanticBaseModel
|
|
71
|
+
|
|
72
|
+
from activemodel import BaseModel
|
|
71
73
|
from activemodel.mixins import PydanticJSONMixin, TypeIDMixin, TimestampsMixin
|
|
72
74
|
|
|
73
75
|
class SubObject(PydanticBaseModel):
|
|
@@ -81,11 +83,28 @@ class User(
|
|
|
81
83
|
TypeIDMixin("user"),
|
|
82
84
|
table=True
|
|
83
85
|
):
|
|
84
|
-
list_field: list[SubObject] = Field(sa_type=JSONB
|
|
86
|
+
list_field: list[SubObject] = Field(sa_type=JSONB)
|
|
85
87
|
```
|
|
86
88
|
|
|
89
|
+
You'll probably want to query the model. Look ma, no sessions!
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
User.where(id="user_123").all()
|
|
93
|
+
|
|
94
|
+
# or, even better, for this case
|
|
95
|
+
User.one("user_123")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Magically creating sessions for DB operations is one of the main problems this project tackles. Even better, you can set
|
|
99
|
+
a single session object to be used for all DB operations. This is helpful for DB transactions, [specifically rolling back
|
|
100
|
+
DB operations on each test.](#pytest)
|
|
101
|
+
|
|
87
102
|
## Usage
|
|
88
103
|
|
|
104
|
+
### Pytest
|
|
105
|
+
|
|
106
|
+
TODO detail out truncation and transactions
|
|
107
|
+
|
|
89
108
|
### Integrating Alembic
|
|
90
109
|
|
|
91
110
|
`alembic init` will not work out of the box. You need to mutate a handful of files:
|
|
@@ -110,14 +129,14 @@ index 0d07420..a63631c 100644
|
|
|
110
129
|
# Use forward slashes (/) also on windows to provide an os agnostic path
|
|
111
130
|
-script_location = .
|
|
112
131
|
+script_location = migrations
|
|
113
|
-
|
|
132
|
+
|
|
114
133
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
115
134
|
# Uncomment the line below if you want the files to be prepended with date and time
|
|
116
135
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
|
117
136
|
# for all available tokens
|
|
118
137
|
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
119
138
|
+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
|
|
120
|
-
|
|
139
|
+
|
|
121
140
|
# sys.path path, will be prepended to sys.path if present.
|
|
122
141
|
# defaults to the current working directory.
|
|
123
142
|
diff --git i/test/migrations/env.py w/test/migrations/env.py
|
|
@@ -129,12 +148,12 @@ index 36112a3..a1e15c2 100644
|
|
|
129
148
|
+# isort: off
|
|
130
149
|
+
|
|
131
150
|
from logging.config import fileConfig
|
|
132
|
-
|
|
151
|
+
|
|
133
152
|
from sqlalchemy import engine_from_config
|
|
134
153
|
@@ -14,11 +17,17 @@ config = context.config
|
|
135
154
|
if config.config_file_name is not None:
|
|
136
155
|
fileConfig(config.config_file_name)
|
|
137
|
-
|
|
156
|
+
|
|
138
157
|
+from sqlmodel import SQLModel
|
|
139
158
|
+from test.models import *
|
|
140
159
|
+from test.utils import database_url
|
|
@@ -147,7 +166,7 @@ index 36112a3..a1e15c2 100644
|
|
|
147
166
|
# target_metadata = mymodel.Base.metadata
|
|
148
167
|
-target_metadata = None
|
|
149
168
|
+target_metadata = SQLModel.metadata
|
|
150
|
-
|
|
169
|
+
|
|
151
170
|
# other values from the config, defined by the needs of env.py,
|
|
152
171
|
# can be acquired:
|
|
153
172
|
diff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako
|
|
@@ -155,13 +174,13 @@ index fbc4b07..9dc78bb 100644
|
|
|
155
174
|
--- i/test/migrations/script.py.mako
|
|
156
175
|
+++ w/test/migrations/script.py.mako
|
|
157
176
|
@@ -9,6 +9,8 @@ from typing import Sequence, Union
|
|
158
|
-
|
|
177
|
+
|
|
159
178
|
from alembic import op
|
|
160
179
|
import sqlalchemy as sa
|
|
161
180
|
+import sqlmodel
|
|
162
181
|
+import activemodel
|
|
163
182
|
${imports if imports else ""}
|
|
164
|
-
|
|
183
|
+
|
|
165
184
|
# revision identifiers, used by Alembic.
|
|
166
185
|
```
|
|
167
186
|
|
|
@@ -178,7 +197,7 @@ This tool is added to all `BaseModel`s and makes it easy to write SQL queries. S
|
|
|
178
197
|
|
|
179
198
|
### Easy Database Sessions
|
|
180
199
|
|
|
181
|
-
I hate the idea f
|
|
200
|
+
I hate the idea f
|
|
182
201
|
|
|
183
202
|
* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.
|
|
184
203
|
* 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 +205,7 @@ I hate the idea f
|
|
|
186
205
|
There are a couple of thorny problems we need to solve for here:
|
|
187
206
|
|
|
188
207
|
* 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
|
-
*
|
|
208
|
+
*
|
|
190
209
|
|
|
191
210
|
https://github.com/tomwojcik/starlette-context
|
|
192
211
|
|
|
@@ -202,8 +221,12 @@ https://github.com/tomwojcik/starlette-context
|
|
|
202
221
|
SQLModel & SQLAlchemy are tricky. Here are some useful internal tricks:
|
|
203
222
|
|
|
204
223
|
* `__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`
|
|
224
|
+
* `ModelClass.relationship_name.property.local_columns`
|
|
206
225
|
* Get cached fields from a model `object_state(instance).dict.get(field_name)`
|
|
226
|
+
* Set the value on a field, without marking it as dirty `attributes.set_committed_value(instance, field_name, val)`
|
|
227
|
+
* Is a model dirty `instance_state(instance).modified`
|
|
228
|
+
* `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.
|
|
229
|
+
* `get_engine().pool.stats()` is helpful for inspecting connection pools and limits\
|
|
207
230
|
|
|
208
231
|
### TypeID
|
|
209
232
|
|
|
@@ -267,6 +290,7 @@ https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d
|
|
|
267
290
|
|
|
268
291
|
* https://github.com/woofz/sqlmodel-basecrud
|
|
269
292
|
* https://github.com/0xthiagomartins/sqlmodel-controller
|
|
293
|
+
* https://github.com/litestar-org/advanced-alchemy?tab=readme-ov-file
|
|
270
294
|
|
|
271
295
|
## Inspiration
|
|
272
296
|
|
|
@@ -279,4 +303,4 @@ https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d
|
|
|
279
303
|
|
|
280
304
|
## Upstream Changes
|
|
281
305
|
|
|
282
|
-
- [ ] https://github.com/fastapi/sqlmodel/pull/1293
|
|
306
|
+
- [ ] https://github.com/fastapi/sqlmodel/pull/1293
|
|
@@ -44,15 +44,17 @@ from sqlmodel import SQLModel
|
|
|
44
44
|
|
|
45
45
|
SQLModel.metadata.create_all(get_engine())
|
|
46
46
|
|
|
47
|
-
# now you can create a user!
|
|
47
|
+
# now you can create a user! without managing sessions!
|
|
48
48
|
User(a_field="a").save()
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Maybe you like JSON:
|
|
52
52
|
|
|
53
53
|
```python
|
|
54
|
-
from
|
|
54
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
55
55
|
from pydantic import BaseModel as PydanticBaseModel
|
|
56
|
+
|
|
57
|
+
from activemodel import BaseModel
|
|
56
58
|
from activemodel.mixins import PydanticJSONMixin, TypeIDMixin, TimestampsMixin
|
|
57
59
|
|
|
58
60
|
class SubObject(PydanticBaseModel):
|
|
@@ -66,11 +68,28 @@ class User(
|
|
|
66
68
|
TypeIDMixin("user"),
|
|
67
69
|
table=True
|
|
68
70
|
):
|
|
69
|
-
list_field: list[SubObject] = Field(sa_type=JSONB
|
|
71
|
+
list_field: list[SubObject] = Field(sa_type=JSONB)
|
|
70
72
|
```
|
|
71
73
|
|
|
74
|
+
You'll probably want to query the model. Look ma, no sessions!
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
User.where(id="user_123").all()
|
|
78
|
+
|
|
79
|
+
# or, even better, for this case
|
|
80
|
+
User.one("user_123")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Magically creating sessions for DB operations is one of the main problems this project tackles. Even better, you can set
|
|
84
|
+
a single session object to be used for all DB operations. This is helpful for DB transactions, [specifically rolling back
|
|
85
|
+
DB operations on each test.](#pytest)
|
|
86
|
+
|
|
72
87
|
## Usage
|
|
73
88
|
|
|
89
|
+
### Pytest
|
|
90
|
+
|
|
91
|
+
TODO detail out truncation and transactions
|
|
92
|
+
|
|
74
93
|
### Integrating Alembic
|
|
75
94
|
|
|
76
95
|
`alembic init` will not work out of the box. You need to mutate a handful of files:
|
|
@@ -95,14 +114,14 @@ index 0d07420..a63631c 100644
|
|
|
95
114
|
# Use forward slashes (/) also on windows to provide an os agnostic path
|
|
96
115
|
-script_location = .
|
|
97
116
|
+script_location = migrations
|
|
98
|
-
|
|
117
|
+
|
|
99
118
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
100
119
|
# Uncomment the line below if you want the files to be prepended with date and time
|
|
101
120
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
|
102
121
|
# for all available tokens
|
|
103
122
|
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
104
123
|
+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
|
|
105
|
-
|
|
124
|
+
|
|
106
125
|
# sys.path path, will be prepended to sys.path if present.
|
|
107
126
|
# defaults to the current working directory.
|
|
108
127
|
diff --git i/test/migrations/env.py w/test/migrations/env.py
|
|
@@ -114,12 +133,12 @@ index 36112a3..a1e15c2 100644
|
|
|
114
133
|
+# isort: off
|
|
115
134
|
+
|
|
116
135
|
from logging.config import fileConfig
|
|
117
|
-
|
|
136
|
+
|
|
118
137
|
from sqlalchemy import engine_from_config
|
|
119
138
|
@@ -14,11 +17,17 @@ config = context.config
|
|
120
139
|
if config.config_file_name is not None:
|
|
121
140
|
fileConfig(config.config_file_name)
|
|
122
|
-
|
|
141
|
+
|
|
123
142
|
+from sqlmodel import SQLModel
|
|
124
143
|
+from test.models import *
|
|
125
144
|
+from test.utils import database_url
|
|
@@ -132,7 +151,7 @@ index 36112a3..a1e15c2 100644
|
|
|
132
151
|
# target_metadata = mymodel.Base.metadata
|
|
133
152
|
-target_metadata = None
|
|
134
153
|
+target_metadata = SQLModel.metadata
|
|
135
|
-
|
|
154
|
+
|
|
136
155
|
# other values from the config, defined by the needs of env.py,
|
|
137
156
|
# can be acquired:
|
|
138
157
|
diff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako
|
|
@@ -140,13 +159,13 @@ index fbc4b07..9dc78bb 100644
|
|
|
140
159
|
--- i/test/migrations/script.py.mako
|
|
141
160
|
+++ w/test/migrations/script.py.mako
|
|
142
161
|
@@ -9,6 +9,8 @@ from typing import Sequence, Union
|
|
143
|
-
|
|
162
|
+
|
|
144
163
|
from alembic import op
|
|
145
164
|
import sqlalchemy as sa
|
|
146
165
|
+import sqlmodel
|
|
147
166
|
+import activemodel
|
|
148
167
|
${imports if imports else ""}
|
|
149
|
-
|
|
168
|
+
|
|
150
169
|
# revision identifiers, used by Alembic.
|
|
151
170
|
```
|
|
152
171
|
|
|
@@ -163,7 +182,7 @@ This tool is added to all `BaseModel`s and makes it easy to write SQL queries. S
|
|
|
163
182
|
|
|
164
183
|
### Easy Database Sessions
|
|
165
184
|
|
|
166
|
-
I hate the idea f
|
|
185
|
+
I hate the idea f
|
|
167
186
|
|
|
168
187
|
* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.
|
|
169
188
|
* 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 +190,7 @@ I hate the idea f
|
|
|
171
190
|
There are a couple of thorny problems we need to solve for here:
|
|
172
191
|
|
|
173
192
|
* 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
|
-
*
|
|
193
|
+
*
|
|
175
194
|
|
|
176
195
|
https://github.com/tomwojcik/starlette-context
|
|
177
196
|
|
|
@@ -187,8 +206,12 @@ https://github.com/tomwojcik/starlette-context
|
|
|
187
206
|
SQLModel & SQLAlchemy are tricky. Here are some useful internal tricks:
|
|
188
207
|
|
|
189
208
|
* `__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`
|
|
209
|
+
* `ModelClass.relationship_name.property.local_columns`
|
|
191
210
|
* Get cached fields from a model `object_state(instance).dict.get(field_name)`
|
|
211
|
+
* Set the value on a field, without marking it as dirty `attributes.set_committed_value(instance, field_name, val)`
|
|
212
|
+
* Is a model dirty `instance_state(instance).modified`
|
|
213
|
+
* `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.
|
|
214
|
+
* `get_engine().pool.stats()` is helpful for inspecting connection pools and limits\
|
|
192
215
|
|
|
193
216
|
### TypeID
|
|
194
217
|
|
|
@@ -252,6 +275,7 @@ https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d
|
|
|
252
275
|
|
|
253
276
|
* https://github.com/woofz/sqlmodel-basecrud
|
|
254
277
|
* https://github.com/0xthiagomartins/sqlmodel-controller
|
|
278
|
+
* https://github.com/litestar-org/advanced-alchemy?tab=readme-ov-file
|
|
255
279
|
|
|
256
280
|
## Inspiration
|
|
257
281
|
|
|
@@ -264,4 +288,4 @@ https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d
|
|
|
264
288
|
|
|
265
289
|
## Upstream Changes
|
|
266
290
|
|
|
267
|
-
- [ ] https://github.com/fastapi/sqlmodel/pull/1293
|
|
291
|
+
- [ ] https://github.com/fastapi/sqlmodel/pull/1293
|
|
@@ -10,9 +10,12 @@ Docs are bad:
|
|
|
10
10
|
|
|
11
11
|
TODO
|
|
12
12
|
|
|
13
|
+
- [ ] nested sessions, maybe only with connections
|
|
14
|
+
- [ ] tests for polyfactory support, setting session across all factories
|
|
13
15
|
- [ ] sessions in tests, they don't work right now
|
|
14
16
|
- [ ] snake case for attributes https://github.com/sqlalchemy/sqlalchemy/issues/7149
|
|
15
|
-
|
|
17
|
+
- [ ] compound unique constraints don't have great names
|
|
18
|
+
- [ ] https://grok.com/share/bGVnYWN5_7bfd5140-2351-4e4f-a8ba-bdedb59ac1e1 figure out better json tracking
|
|
16
19
|
|
|
17
20
|
find_or_create
|
|
18
21
|
|
|
@@ -50,3 +53,14 @@ clear engine
|
|
|
50
53
|
# _engine = None
|
|
51
54
|
# _connection = None
|
|
52
55
|
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
There should be a better way to add unique keys
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
__table_args__ = (
|
|
63
|
+
# users should never see two schemas with the same name
|
|
64
|
+
UniqueConstraint("note_type", "doctor_id", name="doctor_and_note_type_unique"),
|
|
65
|
+
)
|
|
66
|
+
```
|
|
@@ -2,10 +2,11 @@ import json
|
|
|
2
2
|
import typing as t
|
|
3
3
|
from uuid import UUID
|
|
4
4
|
|
|
5
|
-
import pydash
|
|
6
5
|
import sqlalchemy as sa
|
|
7
6
|
import sqlmodel as sm
|
|
7
|
+
import textcase
|
|
8
8
|
from sqlalchemy import Connection, event
|
|
9
|
+
from sqlalchemy.dialects.postgresql import insert as postgres_insert
|
|
9
10
|
from sqlalchemy.orm import Mapper, declared_attr
|
|
10
11
|
from sqlalchemy.orm.attributes import flag_modified as sa_flag_modified
|
|
11
12
|
from sqlalchemy.orm.base import instance_state
|
|
@@ -19,7 +20,6 @@ from . import get_column_from_field_patch # noqa: F401
|
|
|
19
20
|
from .logger import logger
|
|
20
21
|
from .query_wrapper import QueryWrapper
|
|
21
22
|
from .session_manager import get_session
|
|
22
|
-
from sqlalchemy.dialects.postgresql import insert as postgres_insert
|
|
23
23
|
|
|
24
24
|
POSTGRES_INDEXES_NAMING_CONVENTION = {
|
|
25
25
|
"ix": "%(column_0_label)s_idx",
|
|
@@ -152,19 +152,19 @@ class BaseModel(SQLModel):
|
|
|
152
152
|
@declared_attr
|
|
153
153
|
def __tablename__(cls) -> str:
|
|
154
154
|
"""
|
|
155
|
-
Automatically generates the table name for the model by converting the class name from camel case to snake case.
|
|
156
|
-
This is the recommended
|
|
155
|
+
Automatically generates the table name for the model by converting the model's class name from camel case to snake case.
|
|
156
|
+
This is the recommended text case style for table names:
|
|
157
157
|
|
|
158
158
|
https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_upper_case_table_or_column_names
|
|
159
159
|
|
|
160
|
-
By default, the class is lower cased which makes it harder to read.
|
|
160
|
+
By default, the model's class name is lower cased which makes it harder to read.
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
Also, many text case conversion libraries struggle handling words like "LLMCache", this is why we are using
|
|
163
|
+
a more precise library which processes such acronyms: [`textcase`](https://pypi.org/project/textcase/).
|
|
164
164
|
|
|
165
165
|
https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
|
|
166
166
|
"""
|
|
167
|
-
return
|
|
167
|
+
return textcase.snake(cls.__name__)
|
|
168
168
|
|
|
169
169
|
@classmethod
|
|
170
170
|
def foreign_key(cls, **kwargs):
|
|
@@ -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
|
|
|
@@ -233,6 +234,8 @@ class BaseModel(SQLModel):
|
|
|
233
234
|
return result
|
|
234
235
|
|
|
235
236
|
def delete(self):
|
|
237
|
+
"Delete record completely from the database"
|
|
238
|
+
|
|
236
239
|
with get_session() as session:
|
|
237
240
|
if old_session := Session.object_session(self):
|
|
238
241
|
old_session.expunge(self)
|
|
@@ -403,6 +406,19 @@ class BaseModel(SQLModel):
|
|
|
403
406
|
with get_session() as session:
|
|
404
407
|
return session.exec(statement).first()
|
|
405
408
|
|
|
409
|
+
@classmethod
|
|
410
|
+
def one_or_none(cls, *args: t.Any, **kwargs: t.Any):
|
|
411
|
+
"""
|
|
412
|
+
Gets a single record from the database. Pass an PK ID or a kwarg to filter by.
|
|
413
|
+
Returns None if no record is found. Throws an error if more than one record is found.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
args, kwargs = cls.__process_filter_args__(*args, **kwargs)
|
|
417
|
+
statement = select(cls).filter(*args).filter_by(**kwargs)
|
|
418
|
+
|
|
419
|
+
with get_session() as session:
|
|
420
|
+
return session.exec(statement).one_or_none()
|
|
421
|
+
|
|
406
422
|
@classmethod
|
|
407
423
|
def one(cls, *args: t.Any, **kwargs: t.Any):
|
|
408
424
|
"""
|