activemodel 0.14.0__tar.gz → 0.15.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.14.0 → activemodel-0.15.0}/.github/dependabot.yml +2 -2
- {activemodel-0.14.0 → activemodel-0.15.0}/.github/workflows/build_and_publish.yml +21 -7
- {activemodel-0.14.0 → activemodel-0.15.0}/.github/workflows/repo-sync.yml +2 -4
- {activemodel-0.14.0 → activemodel-0.15.0}/CHANGELOG.md +20 -0
- activemodel-0.15.0/Justfile +105 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/PKG-INFO +4 -4
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/base_model.py +4 -6
- activemodel-0.15.0/activemodel/patches/__init__.py +2 -0
- {activemodel-0.14.0/activemodel → activemodel-0.15.0/activemodel/patches}/get_column_from_field_patch.py +29 -40
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/pytest/factories.py +12 -1
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/pytest/plugin.py +11 -7
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/pytest/transaction.py +7 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/pytest/truncate.py +0 -1
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/types/sqlalchemy_protocol.py +1 -3
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/types/sqlalchemy_protocol.pyi +1 -1
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/types/typeid_patch.py +6 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/docker-compose.yml +2 -2
- {activemodel-0.14.0 → activemodel-0.15.0}/pyproject.toml +9 -7
- {activemodel-0.14.0 → activemodel-0.15.0}/test/models.py +1 -1
- {activemodel-0.14.0 → activemodel-0.15.0}/test/orm/test_upsert.py +4 -5
- {activemodel-0.14.0 → activemodel-0.15.0}/test/orm_test.py +1 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/pytest/pytest_test.py +1 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/types/typeid_mixin_test.py +0 -5
- {activemodel-0.14.0 → activemodel-0.15.0}/test/types/typeid_sqlmodel_test.py +0 -2
- {activemodel-0.14.0 → activemodel-0.15.0}/test/utils.py +0 -1
- activemodel-0.15.0/uv.lock +1544 -0
- activemodel-0.14.0/Justfile +0 -14
- activemodel-0.14.0/uv.lock +0 -1362
- {activemodel-0.14.0 → activemodel-0.15.0}/.envrc +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/.gitignore +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/.tool-versions +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/.vscode/settings.json +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/LICENSE +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/Makefile +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/README.md +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/TODO +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/__init__.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/celery.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/cli/__init__.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/errors.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/logger.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/mixins/__init__.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/mixins/pydantic_json.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/mixins/soft_delete.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/mixins/timestamps.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/mixins/typeid.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/pytest/__init__.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/query_wrapper.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/session_manager.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/types/__init__.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/types/typeid.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/activemodel/utils.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground/alternative_typeid_mixin.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground/comments.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground/env-with-model.patch +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground/extract_comments.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground/field.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground/middleware.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground/old_session_manager.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground/pydantic_validation.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/playground.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/__init__.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/comments_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/conftest.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/delete_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/factory_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/fastapi_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/import_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/lifecycle_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/migrations/README +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/migrations/alembic.ini +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/migrations/env.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/migrations/script.py.mako +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/migrations_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/mutation_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/nested_pydantic_json_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/session_manager_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/table_name_test.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/test_query_wrapper.py +0 -0
- {activemodel-0.14.0 → activemodel-0.15.0}/test/types/typeid_pydantic_test.py +0 -0
|
@@ -32,25 +32,39 @@ jobs:
|
|
|
32
32
|
|
|
33
33
|
publish:
|
|
34
34
|
runs-on: ubuntu-latest
|
|
35
|
-
needs:
|
|
35
|
+
needs: release-please
|
|
36
36
|
if: needs.release-please.outputs.release_created
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
contents: read
|
|
37
40
|
steps:
|
|
38
|
-
- uses: actions/checkout@
|
|
41
|
+
- uses: actions/checkout@v6
|
|
39
42
|
- uses: jdx/mise-action@v3
|
|
40
|
-
-
|
|
43
|
+
- uses: iloveitaly/github-action-direnv-load-and-mask@master
|
|
44
|
+
- run: uv sync
|
|
41
45
|
- run: uv build
|
|
42
|
-
- run: uv publish
|
|
46
|
+
- run: uv publish
|
|
47
|
+
|
|
48
|
+
lint:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v6
|
|
52
|
+
- uses: jdx/mise-action@v3
|
|
53
|
+
env:
|
|
54
|
+
MISE_ENV: ci
|
|
55
|
+
- uses: iloveitaly/github-action-direnv-load-and-mask@master
|
|
56
|
+
- run: uv sync
|
|
57
|
+
# - run: just lint
|
|
43
58
|
|
|
44
59
|
matrix-test:
|
|
45
60
|
strategy:
|
|
46
61
|
matrix:
|
|
47
62
|
os: [ubuntu-latest]
|
|
48
|
-
# TODO test on macos-latest, does not have docker by default :/
|
|
49
63
|
# unfortunately, some of the typing stuff we use requires new python versions
|
|
50
|
-
python-version: ["3.13", "3.12"]
|
|
64
|
+
python-version: ["3.14", "3.13", "3.12"]
|
|
51
65
|
runs-on: ${{ matrix.os }}
|
|
52
66
|
steps:
|
|
53
|
-
- uses: actions/checkout@
|
|
67
|
+
- uses: actions/checkout@v6
|
|
54
68
|
- uses: jdx/mise-action@v3
|
|
55
69
|
- run: mise use python@${{ matrix.python-version }}
|
|
56
70
|
- run: docker compose up -d --wait
|
|
@@ -8,9 +8,7 @@ jobs:
|
|
|
8
8
|
repo_sync:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
- name: Repository Metadata Sync
|
|
14
|
-
uses: iloveitaly/github-actions-metadata-sync@main
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
- uses: iloveitaly/github-actions-metadata-sync@main
|
|
15
13
|
with:
|
|
16
14
|
TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.15.0](https://github.com/iloveitaly/activemodel/compare/v0.14.1...v0.15.0) (2026-02-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* sqlmodel 0.0.32 support ([#33](https://github.com/iloveitaly/activemodel/issues/33)) ([e2a104a](https://github.com/iloveitaly/activemodel/commit/e2a104a11cd1f530e52aa0d6b2e951752ef2bb92))
|
|
9
|
+
|
|
10
|
+
## [0.14.1](https://github.com/iloveitaly/activemodel/compare/v0.14.0...v0.14.1) (2026-01-14)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* work with latest sqlmodel which drops v1 ([79920c6](https://github.com/iloveitaly/activemodel/commit/79920c63913b0a1d6824efc84719e6ea41b483eb))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* add pydantic v2 compatibility notes to typeid_patch.py ([7d3f43a](https://github.com/iloveitaly/activemodel/commit/7d3f43a0b1a3c2c51d46ef0530aabee7392f75ec))
|
|
21
|
+
* add warning log for missing session in ActiveModelFactory ([317dd22](https://github.com/iloveitaly/activemodel/commit/317dd22b4abe5d9261d8c7f69a7b32b9c7b2d4de))
|
|
22
|
+
|
|
3
23
|
## [0.14.0](https://github.com/iloveitaly/activemodel/compare/v0.13.0...v0.14.0) (2025-10-16)
|
|
4
24
|
|
|
5
25
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
set unstable := true
|
|
2
|
+
|
|
3
|
+
[macos]
|
|
4
|
+
db_play:
|
|
5
|
+
uv tool run pgcli $DATABASE_URL
|
|
6
|
+
|
|
7
|
+
up:
|
|
8
|
+
docker compose up -d
|
|
9
|
+
|
|
10
|
+
# Run linting checks
|
|
11
|
+
[script]
|
|
12
|
+
lint FILES=".":
|
|
13
|
+
set +e
|
|
14
|
+
exit_code=0
|
|
15
|
+
|
|
16
|
+
if [ -n "${CI:-}" ]; then
|
|
17
|
+
# CI mode: GitHub-friendly output
|
|
18
|
+
uv run ruff check --output-format=github {{FILES}} || exit_code=$?
|
|
19
|
+
uv run ruff format --check {{FILES}} || exit_code=$?
|
|
20
|
+
|
|
21
|
+
uv run pyright {{FILES}} --outputjson > pyright_report.json || exit_code=$?
|
|
22
|
+
jq -r \
|
|
23
|
+
--arg root "$GITHUB_WORKSPACE/" \
|
|
24
|
+
'
|
|
25
|
+
.generalDiagnostics[] |
|
|
26
|
+
.file as $file |
|
|
27
|
+
($file | sub("^\\Q\($root)\\E"; "")) as $rel_file |
|
|
28
|
+
"::\(.severity) file=\($rel_file),line=\(.range.start.line),endLine=\(.range.end.line),col=\(.range.start.character),endColumn=\(.range.end.character)::\($rel_file):\(.range.start.line): \(.message)"
|
|
29
|
+
' < pyright_report.json
|
|
30
|
+
rm pyright_report.json
|
|
31
|
+
else
|
|
32
|
+
# Local mode: regular output
|
|
33
|
+
uv run ruff check {{FILES}} || exit_code=$?
|
|
34
|
+
uv run ruff format --check {{FILES}} || exit_code=$?
|
|
35
|
+
uv run pyright {{FILES}} || exit_code=$?
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [ $exit_code -ne 0 ]; then
|
|
39
|
+
echo "One or more linting checks failed"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Automatically fix linting errors
|
|
44
|
+
lint-fix:
|
|
45
|
+
uv run ruff check . --fix
|
|
46
|
+
uv run ruff format .
|
|
47
|
+
|
|
48
|
+
clean:
|
|
49
|
+
rm -rf *.egg-info .venv
|
|
50
|
+
find . -type d -name "__pycache__" -prune -exec rm -rf {} \; 2>/dev/null || true
|
|
51
|
+
|
|
52
|
+
# TODO what exactly was this used for?
|
|
53
|
+
gh_configure:
|
|
54
|
+
repo_path=$(gh repo view --json nameWithOwner --jq '.nameWithOwner') && \
|
|
55
|
+
gh api --method PUT "/repos/${repo_path}/actions/permissions/workflow" \
|
|
56
|
+
-f default_workflow_permissions=write \
|
|
57
|
+
-F can_approve_pull_request_reviews=true && \
|
|
58
|
+
gh api "/repos/${repo_path}/actions/permissions/workflow"
|
|
59
|
+
|
|
60
|
+
GITHUB_PROTECT_MASTER_RULESET := """
|
|
61
|
+
{
|
|
62
|
+
"name": "Protect master from force pushes",
|
|
63
|
+
"target": "branch",
|
|
64
|
+
"enforcement": "active",
|
|
65
|
+
"conditions": {
|
|
66
|
+
"ref_name": {
|
|
67
|
+
"include": ["refs/heads/master"],
|
|
68
|
+
"exclude": []
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"rules": [
|
|
72
|
+
{
|
|
73
|
+
"type": "non_fast_forward"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
_github_repo:
|
|
80
|
+
gh repo view --json nameWithOwner -q .nameWithOwner
|
|
81
|
+
|
|
82
|
+
# TODO this only supports deleting the single ruleset specified above
|
|
83
|
+
github_ruleset_protect_master_delete:
|
|
84
|
+
repo=$(just _github_repo) && \
|
|
85
|
+
ruleset_name=$(echo '{{GITHUB_PROTECT_MASTER_RULESET}}' | jq -r .name) && \
|
|
86
|
+
ruleset_id=$(gh api repos/$repo/rulesets --jq ".[] | select(.name == \"$ruleset_name\") | .id") && \
|
|
87
|
+
(([ -n "${ruleset_id}" ] || (echo "No ruleset found" && exit 0)) || gh api --method DELETE repos/$repo/rulesets/$ruleset_id)
|
|
88
|
+
|
|
89
|
+
# adds github ruleset to prevent --force and other destructive actions on the github main branch
|
|
90
|
+
github_ruleset_protect_master_create: github_ruleset_protect_master_delete
|
|
91
|
+
gh api --method POST repos/$(just _github_repo)/rulesets --input - <<< '{{GITHUB_PROTECT_MASTER_RULESET}}'
|
|
92
|
+
|
|
93
|
+
INVESTIGATE_PATCHES_PROMPT := '''
|
|
94
|
+
Take a look at this patched function from an external package. The assertion that is in place is breaking because the upstream package was mutated.
|
|
95
|
+
|
|
96
|
+
I want you to investigate the changes in that function between The current version of that package that is used in this project and the most recent version. Take a look at our patch and:
|
|
97
|
+
|
|
98
|
+
1. Check the latest version of the package in pypi and update the version reference in pyproject.toml and `uv sync`
|
|
99
|
+
2. Update the patch if necessary to be compatible with the latest logic in the upstream package.
|
|
100
|
+
3. Provide a summary of the changes made in the latest version of that function.
|
|
101
|
+
'''
|
|
102
|
+
|
|
103
|
+
# Output the investigate patches prompt
|
|
104
|
+
investigate-patches-prompt:
|
|
105
|
+
@echo '{{INVESTIGATE_PATCHES_PROMPT}}'
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: activemodel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.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
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
10
|
Requires-Dist: python-decouple-typed>=3.11.0
|
|
11
|
-
Requires-Dist: sqlmodel
|
|
11
|
+
Requires-Dist: sqlmodel==0.0.32
|
|
12
12
|
Requires-Dist: textcase>=0.4.0
|
|
13
|
-
Requires-Dist: typeid-python
|
|
13
|
+
Requires-Dist: typeid-python==0.3.3
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
|
|
16
16
|
# ActiveModel: ORM Wrapper for SQLModel
|
|
@@ -15,7 +15,7 @@ from sqlalchemy.orm import declared_attr
|
|
|
15
15
|
from activemodel.mixins.pydantic_json import PydanticJSONMixin
|
|
16
16
|
|
|
17
17
|
# NOTE: this patches a core method in sqlmodel to support db comments
|
|
18
|
-
from . import get_column_from_field_patch # noqa: F401
|
|
18
|
+
from .patches import get_column_from_field_patch # noqa: F401
|
|
19
19
|
from .query_wrapper import QueryWrapper
|
|
20
20
|
from .session_manager import get_session
|
|
21
21
|
|
|
@@ -72,11 +72,9 @@ class BaseModel(SQLModel):
|
|
|
72
72
|
def __init_subclass__(cls, **kwargs):
|
|
73
73
|
super().__init_subclass__(**kwargs)
|
|
74
74
|
|
|
75
|
-
from sqlmodel._compat import set_config_value
|
|
76
|
-
|
|
77
75
|
# Enables field-level docstrings on the pydantic `description` field, which we
|
|
78
76
|
# copy into table/column comments by patching SQLModel internals elsewhere.
|
|
79
|
-
|
|
77
|
+
cls.model_config["use_attribute_docstrings"] = True
|
|
80
78
|
|
|
81
79
|
cls._apply_class_doc()
|
|
82
80
|
|
|
@@ -290,7 +288,7 @@ class BaseModel(SQLModel):
|
|
|
290
288
|
# TODO where is this actually used? shoudl prob remove this
|
|
291
289
|
# TODO should we even do this? Can we specify a better json rendering class?
|
|
292
290
|
def json(self, **kwargs):
|
|
293
|
-
return json.dumps(self.
|
|
291
|
+
return json.dumps(self.model_dump(), default=str, **kwargs)
|
|
294
292
|
|
|
295
293
|
# TODO should move this to the wrapper
|
|
296
294
|
@classmethod
|
|
@@ -325,7 +323,7 @@ class BaseModel(SQLModel):
|
|
|
325
323
|
assert len(args) > 0, "Must pass at least one field name"
|
|
326
324
|
|
|
327
325
|
for field_name in args:
|
|
328
|
-
if field_name not in self.
|
|
326
|
+
if field_name not in self.model_fields:
|
|
329
327
|
raise ValueError(f"Field '{field_name}' does not exist in the model.")
|
|
330
328
|
|
|
331
329
|
# check if the field exists
|
|
@@ -13,26 +13,23 @@ Some ideas for this originally sourced from: https://github.com/fastapi/sqlmodel
|
|
|
13
13
|
from typing import (
|
|
14
14
|
TYPE_CHECKING,
|
|
15
15
|
Any,
|
|
16
|
-
Dict,
|
|
17
16
|
Sequence,
|
|
18
17
|
cast,
|
|
19
18
|
)
|
|
20
19
|
|
|
21
20
|
import sqlmodel
|
|
22
|
-
from pydantic.fields import FieldInfo as PydanticFieldInfo
|
|
23
21
|
from sqlalchemy import (
|
|
24
22
|
Column,
|
|
25
23
|
ForeignKey,
|
|
26
24
|
)
|
|
27
25
|
from sqlmodel._compat import ( # type: ignore[attr-defined]
|
|
28
|
-
IS_PYDANTIC_V2,
|
|
29
26
|
ModelMetaclass,
|
|
30
27
|
Representation,
|
|
31
28
|
Undefined,
|
|
32
29
|
UndefinedType,
|
|
33
30
|
is_field_noneable,
|
|
34
31
|
)
|
|
35
|
-
from sqlmodel.main import
|
|
32
|
+
from sqlmodel.main import get_sqlalchemy_type, _get_sqlmodel_field_value
|
|
36
33
|
|
|
37
34
|
from activemodel.utils import hash_function_code
|
|
38
35
|
|
|
@@ -43,67 +40,60 @@ if TYPE_CHECKING:
|
|
|
43
40
|
from pydantic_core import PydanticUndefinedType as UndefinedType
|
|
44
41
|
|
|
45
42
|
|
|
43
|
+
# https://github.com/fastapi/sqlmodel/blob/5c2dbe419edc2d15200eee5269c9508987944ed8/sqlmodel/main.py#L691
|
|
46
44
|
assert (
|
|
47
45
|
hash_function_code(sqlmodel.main.get_column_from_field)
|
|
48
|
-
== "
|
|
46
|
+
== "c64e50f8ca8a345ad2543690849a284d5436515835e41c56638cfaba251bc406"
|
|
47
|
+
), (
|
|
48
|
+
f"get_column_from_field has changed, please verify the patch is still valid: {hash_function_code(sqlmodel.main.get_column_from_field)}"
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def get_column_from_field(field:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
and converts it into a sqlalchemy Column object.
|
|
56
|
-
"""
|
|
57
|
-
if IS_PYDANTIC_V2:
|
|
58
|
-
field_info = field
|
|
59
|
-
else:
|
|
60
|
-
field_info = field.field_info
|
|
61
|
-
|
|
62
|
-
sa_column = getattr(field_info, "sa_column", Undefined)
|
|
52
|
+
def get_column_from_field(field: Any) -> Column: # type: ignore
|
|
53
|
+
field_info = field
|
|
54
|
+
sa_column = _get_sqlmodel_field_value(field_info, "sa_column", Undefined)
|
|
63
55
|
if isinstance(sa_column, Column):
|
|
64
|
-
#
|
|
56
|
+
# <Change>
|
|
65
57
|
if not sa_column.comment and (field_comment := field_info.description):
|
|
66
58
|
sa_column.comment = field_comment
|
|
59
|
+
# </Change>
|
|
67
60
|
return sa_column
|
|
68
|
-
|
|
69
|
-
primary_key =
|
|
61
|
+
sa_type = get_sqlalchemy_type(field)
|
|
62
|
+
primary_key = _get_sqlmodel_field_value(field_info, "primary_key", Undefined)
|
|
70
63
|
if primary_key is Undefined:
|
|
71
64
|
primary_key = False
|
|
72
|
-
|
|
73
|
-
index = getattr(field_info, "index", Undefined)
|
|
65
|
+
index = _get_sqlmodel_field_value(field_info, "index", Undefined)
|
|
74
66
|
if index is Undefined:
|
|
75
67
|
index = False
|
|
76
|
-
|
|
77
68
|
nullable = not primary_key and is_field_noneable(field)
|
|
78
69
|
# Override derived nullability if the nullable property is set explicitly
|
|
79
70
|
# on the field
|
|
80
|
-
field_nullable =
|
|
71
|
+
field_nullable = _get_sqlmodel_field_value(field_info, "nullable", Undefined) # noqa: B009
|
|
81
72
|
if field_nullable is not Undefined:
|
|
82
73
|
assert not isinstance(field_nullable, UndefinedType)
|
|
83
74
|
nullable = field_nullable
|
|
84
75
|
args = []
|
|
85
|
-
foreign_key =
|
|
76
|
+
foreign_key = _get_sqlmodel_field_value(field_info, "foreign_key", Undefined)
|
|
86
77
|
if foreign_key is Undefined:
|
|
87
78
|
foreign_key = None
|
|
88
|
-
unique =
|
|
79
|
+
unique = _get_sqlmodel_field_value(field_info, "unique", Undefined)
|
|
89
80
|
if unique is Undefined:
|
|
90
81
|
unique = False
|
|
91
82
|
if foreign_key:
|
|
92
|
-
|
|
83
|
+
ondelete_value = _get_sqlmodel_field_value(field_info, "ondelete", Undefined)
|
|
84
|
+
if ondelete_value is Undefined:
|
|
85
|
+
ondelete_value = None
|
|
86
|
+
if ondelete_value == "SET NULL" and not nullable:
|
|
93
87
|
raise RuntimeError('ondelete="SET NULL" requires nullable=True')
|
|
94
88
|
assert isinstance(foreign_key, str)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ondelete = None
|
|
98
|
-
assert isinstance(ondelete, (str, type(None))) # for typing
|
|
99
|
-
args.append(ForeignKey(foreign_key, ondelete=ondelete))
|
|
89
|
+
assert isinstance(ondelete_value, (str, type(None))) # for typing
|
|
90
|
+
args.append(ForeignKey(foreign_key, ondelete=ondelete_value))
|
|
100
91
|
kwargs = {
|
|
101
92
|
"primary_key": primary_key,
|
|
102
93
|
"nullable": nullable,
|
|
103
94
|
"index": index,
|
|
104
95
|
"unique": unique,
|
|
105
96
|
}
|
|
106
|
-
|
|
107
97
|
sa_default = Undefined
|
|
108
98
|
if field_info.default_factory:
|
|
109
99
|
sa_default = field_info.default_factory
|
|
@@ -111,14 +101,14 @@ def get_column_from_field(field: PydanticFieldInfo | FieldInfo) -> Column: # ty
|
|
|
111
101
|
sa_default = field_info.default
|
|
112
102
|
if sa_default is not Undefined:
|
|
113
103
|
kwargs["default"] = sa_default
|
|
114
|
-
|
|
115
|
-
sa_column_args = getattr(field_info, "sa_column_args", Undefined)
|
|
104
|
+
sa_column_args = _get_sqlmodel_field_value(field_info, "sa_column_args", Undefined)
|
|
116
105
|
if sa_column_args is not Undefined:
|
|
117
106
|
args.extend(list(cast(Sequence[Any], sa_column_args)))
|
|
107
|
+
sa_column_kwargs = _get_sqlmodel_field_value(
|
|
108
|
+
field_info, "sa_column_kwargs", Undefined
|
|
109
|
+
)
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# IMPORTANT: change from the original function
|
|
111
|
+
# <Change>
|
|
122
112
|
if field_info.description:
|
|
123
113
|
if sa_column_kwargs is Undefined:
|
|
124
114
|
sa_column_kwargs = {}
|
|
@@ -128,11 +118,10 @@ def get_column_from_field(field: PydanticFieldInfo | FieldInfo) -> Column: # ty
|
|
|
128
118
|
# only update comments if not already set
|
|
129
119
|
if "comment" not in sa_column_kwargs:
|
|
130
120
|
sa_column_kwargs["comment"] = field_info.description
|
|
121
|
+
# </Change>
|
|
131
122
|
|
|
132
123
|
if sa_column_kwargs is not Undefined:
|
|
133
|
-
kwargs.update(cast(
|
|
134
|
-
|
|
135
|
-
sa_type = get_sqlalchemy_type(field)
|
|
124
|
+
kwargs.update(cast(dict[Any, Any], sa_column_kwargs))
|
|
136
125
|
return Column(sa_type, *args, **kwargs) # type: ignore
|
|
137
126
|
|
|
138
127
|
|
|
@@ -12,6 +12,7 @@ from polyfactory.field_meta import FieldMeta
|
|
|
12
12
|
from typeid import TypeID
|
|
13
13
|
|
|
14
14
|
from activemodel.session_manager import global_session
|
|
15
|
+
from activemodel.logger import logger
|
|
15
16
|
|
|
16
17
|
# TODO not currently used
|
|
17
18
|
# def type_id_provider(cls, field_meta):
|
|
@@ -56,11 +57,20 @@ class ActiveModelFactory[T](SQLModelFactory[T]):
|
|
|
56
57
|
@classmethod
|
|
57
58
|
def save(cls, *args, **kwargs) -> T:
|
|
58
59
|
"""
|
|
60
|
+
Builds and persists a new model to the database.
|
|
61
|
+
|
|
59
62
|
Where this gets tricky, is this can be called multiple times within the same callstack. This can happen when
|
|
60
|
-
a factory uses other factories to create relationships.
|
|
63
|
+
a factory uses other factories to create relationships. This is fine if `__sqlalchemy_session__` is set, but
|
|
64
|
+
if it's not (in the case of a truncation DB strategy) you'll run into issues.
|
|
61
65
|
|
|
62
66
|
In a truncation strategy, the __sqlalchemy_session__ is set to None.
|
|
63
67
|
"""
|
|
68
|
+
|
|
69
|
+
if cls.__sqlalchemy_session__ is None:
|
|
70
|
+
logger.warning(
|
|
71
|
+
"No __sqlalchemy_session__ set on factory class, nested factory save() will fail. Use `db_session` or `db_truncate_session` to avoid this."
|
|
72
|
+
)
|
|
73
|
+
|
|
64
74
|
with global_session(cls.__sqlalchemy_session__):
|
|
65
75
|
return cls.build(*args, **kwargs).save()
|
|
66
76
|
|
|
@@ -74,6 +84,7 @@ class ActiveModelFactory[T](SQLModelFactory[T]):
|
|
|
74
84
|
# TODO right now assumes the model is typeid, maybe we should assert against this?
|
|
75
85
|
primary_key_name = cls.__model__.primary_key_column().name
|
|
76
86
|
return TypeID(
|
|
87
|
+
# gets the prefix associated with the pk field
|
|
77
88
|
cls.__model__.model_fields[primary_key_name].sa_column.type.prefix
|
|
78
89
|
)
|
|
79
90
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Currently provides:
|
|
4
4
|
|
|
5
|
-
* ``db_session`` fixture
|
|
6
|
-
* ``activemodel_preserve_tables`` ini option
|
|
5
|
+
* ``db_session`` fixture - quick access to a database session (see ``test_session``)
|
|
6
|
+
* ``activemodel_preserve_tables`` ini option - configure tables to preserve when using
|
|
7
7
|
``database_reset_truncate`` (comma separated list or multiple lines depending on config style)
|
|
8
8
|
|
|
9
9
|
Configuration examples:
|
|
@@ -28,7 +28,10 @@ The list always implicitly includes ``alembic_version`` even if not specified.
|
|
|
28
28
|
from activemodel.session_manager import global_session
|
|
29
29
|
import pytest
|
|
30
30
|
|
|
31
|
-
from .transaction import
|
|
31
|
+
from .transaction import (
|
|
32
|
+
set_factory_sessions,
|
|
33
|
+
test_session,
|
|
34
|
+
)
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
def pytest_addoption(
|
|
@@ -53,7 +56,7 @@ def pytest_addoption(
|
|
|
53
56
|
@pytest.fixture(scope="function")
|
|
54
57
|
def db_session():
|
|
55
58
|
"""
|
|
56
|
-
Helpful for tests that are
|
|
59
|
+
Helpful for tests that are similar to unit tests. If you doing a routing or integration test, you
|
|
57
60
|
probably don't need this. If your unit test is simple (you are just creating a couple of models) you
|
|
58
61
|
can most likely skip this.
|
|
59
62
|
|
|
@@ -71,11 +74,12 @@ def db_truncate_session():
|
|
|
71
74
|
"""
|
|
72
75
|
Provides a database session for testing when using a truncation cleaning strategy.
|
|
73
76
|
|
|
74
|
-
When
|
|
77
|
+
When using a truncation cleaning strategy, no global test session is set. This means all models that are created
|
|
78
|
+
are tied to a detached session, which makes it hard to mutate models after creation. This fixture fixes that problem
|
|
79
|
+
by setting the session used by all model factories to a global session.
|
|
75
80
|
"""
|
|
76
81
|
with global_session() as session:
|
|
77
82
|
# set global database sessions for model factories to avoid lazy loading issues
|
|
78
|
-
|
|
79
|
-
set_polyfactory_session(session)
|
|
83
|
+
set_factory_sessions(session)
|
|
80
84
|
|
|
81
85
|
yield session
|
|
@@ -41,6 +41,13 @@ def set_polyfactory_session(session):
|
|
|
41
41
|
ActiveModelFactory.__sqlalchemy_session__ = session
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def set_factory_sessions(session):
|
|
45
|
+
"set all supported model factories to use the provided session"
|
|
46
|
+
|
|
47
|
+
set_factory_session(session)
|
|
48
|
+
set_polyfactory_session(session)
|
|
49
|
+
|
|
50
|
+
|
|
44
51
|
@contextlib.contextmanager
|
|
45
52
|
def test_session():
|
|
46
53
|
"""
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# IMPORTANT: This file is auto-generated. Do not edit directly.
|
|
2
2
|
|
|
3
|
-
from typing import Protocol
|
|
3
|
+
from typing import Protocol
|
|
4
4
|
import sqlmodel as sm
|
|
5
|
-
from sqlalchemy.sql.base import _NoArg
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
class SQLAlchemyQueryMethods[T: sm.SQLModel](Protocol):
|
|
@@ -10,14 +10,14 @@ services:
|
|
|
10
10
|
ports:
|
|
11
11
|
- ${CI:+5432}:5432
|
|
12
12
|
volumes:
|
|
13
|
-
- package_postgres_data:/var/lib/postgresql/
|
|
13
|
+
- package_postgres_data:/var/lib/postgresql/
|
|
14
14
|
healthcheck:
|
|
15
15
|
# if you customize the default user at all, this healthcheck with clutter your logs
|
|
16
16
|
# since the default username + database does not match up
|
|
17
17
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
|
18
18
|
interval: 2s
|
|
19
19
|
timeout: 5s
|
|
20
|
-
retries:
|
|
20
|
+
retries: 5
|
|
21
21
|
|
|
22
22
|
volumes:
|
|
23
23
|
# NOTE redis data is not persisted, assumed to be ephemeral
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "activemodel"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.15.0"
|
|
4
4
|
description = "Make SQLModel more like an a real ORM"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"python-decouple-typed>=3.11.0",
|
|
9
|
-
"sqlmodel
|
|
9
|
+
"sqlmodel==0.0.32",
|
|
10
10
|
"textcase>=0.4.0",
|
|
11
|
-
"typeid-python
|
|
11
|
+
"typeid-python==0.3.3",
|
|
12
12
|
]
|
|
13
13
|
authors = [{ name = "Michael Bianco", email = "iloveitaly@gmail.com" }]
|
|
14
14
|
keywords = ["sqlmodel", "orm", "activerecord", "activemodel", "sqlalchemy"]
|
|
@@ -38,13 +38,15 @@ dev = [
|
|
|
38
38
|
"pydantic>=2.10.0",
|
|
39
39
|
"fastapi[standard]>=0.115.6",
|
|
40
40
|
"alembic>=1.14.1",
|
|
41
|
-
"pyright>=1.1.398",
|
|
41
|
+
"pyright[nodejs]>=1.1.398",
|
|
42
42
|
"polyfactory>=2.22.1",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
[tool.ruff]
|
|
46
|
-
extend-exclude = ["playground.py"]
|
|
47
|
-
|
|
46
|
+
extend-exclude = ["playground.py", "playground/"]
|
|
47
|
+
|
|
48
|
+
[tool.pyright]
|
|
49
|
+
exclude = ["playground/", "tmp/", ".venv/", "tests/"]
|
|
48
50
|
|
|
49
51
|
# https://github.com/astral-sh/uv/issues/9513
|
|
50
52
|
[build-system]
|
|
@@ -4,7 +4,7 @@ Example models to test various ORM cases
|
|
|
4
4
|
|
|
5
5
|
from pydantic import computed_field
|
|
6
6
|
from sqlalchemy import UniqueConstraint
|
|
7
|
-
from sqlmodel import
|
|
7
|
+
from sqlmodel import Field, Relationship
|
|
8
8
|
|
|
9
9
|
from activemodel import BaseModel
|
|
10
10
|
from activemodel.mixins import TypeIDMixin
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from sqlalchemy import UniqueConstraint
|
|
3
|
-
from sqlmodel import Field
|
|
4
1
|
|
|
5
|
-
from activemodel import BaseModel
|
|
6
|
-
from activemodel.mixins.typeid import TypeIDMixin
|
|
7
2
|
from test.models import UpsertTestModel
|
|
8
3
|
|
|
9
4
|
|
|
@@ -45,6 +40,7 @@ def test_upsert_single_unique_field(create_and_wipe_database):
|
|
|
45
40
|
|
|
46
41
|
assert UpsertTestModel.count() == 1
|
|
47
42
|
record = UpsertTestModel.get(name="test1")
|
|
43
|
+
assert record is not None
|
|
48
44
|
# 1. Double-check that DB record matches what was returned
|
|
49
45
|
assert record.id == updated_result.id
|
|
50
46
|
assert record.category == "B"
|
|
@@ -68,6 +64,7 @@ def test_upsert_multiple_unique_fields(create_and_wipe_database):
|
|
|
68
64
|
|
|
69
65
|
# 1. Check that returned model's ID matches the DB record
|
|
70
66
|
db_record1 = UpsertTestModel.get(name="multi1", category="X")
|
|
67
|
+
assert db_record1 is not None
|
|
71
68
|
assert db_record1.id == result1.id
|
|
72
69
|
|
|
73
70
|
result2 = UpsertTestModel.upsert(
|
|
@@ -139,6 +136,7 @@ def test_upsert_single_update_field(create_and_wipe_database):
|
|
|
139
136
|
|
|
140
137
|
# Get record to verify field was updated
|
|
141
138
|
record = UpsertTestModel.get(name="update1")
|
|
139
|
+
assert record is not None
|
|
142
140
|
# 1. Check that DB record matches what was returned
|
|
143
141
|
assert record.id == updated_result.id
|
|
144
142
|
assert record.value == 25 # Updated
|
|
@@ -178,6 +176,7 @@ def test_upsert_multiple_update_fields(create_and_wipe_database):
|
|
|
178
176
|
|
|
179
177
|
# Get record to verify all fields were updated
|
|
180
178
|
record = UpsertTestModel.get(name="update2")
|
|
179
|
+
assert record is not None
|
|
181
180
|
# 1. Check that DB record matches what was returned
|
|
182
181
|
assert record.id == updated_result.id
|
|
183
182
|
assert record.value == 99
|