sqlmodel-ext 0.3.2__tar.gz → 0.3.3__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.
- sqlmodel_ext-0.3.3/.github/workflows/ci.yml +100 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/PKG-INFO +1 -1
- sqlmodel_ext-0.3.3/examples/consumer_integration/run.py +148 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/pyproject.toml +1 -1
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/__init__.py +1 -1
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/_compat.py +13 -3
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/base.py +123 -7
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/__init__.py +23 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/mixins/cached_table.py +71 -36
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/mixins/table.py +182 -33
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/pagination.py +2 -2
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/relation_load_checker.py +97 -9
- sqlmodel_ext-0.3.3/tests/__init__.py +0 -0
- sqlmodel_ext-0.3.3/tests/_models.py +45 -0
- sqlmodel_ext-0.3.3/tests/conftest.py +58 -0
- sqlmodel_ext-0.3.3/tests/test_annotated_default_factory_inheritance.py +194 -0
- sqlmodel_ext-0.3.3/tests/test_basic_crud.py +61 -0
- sqlmodel_ext-0.3.3/tests/test_smoke_imports.py +90 -0
- sqlmodel_ext-0.3.3/tests/test_sti_default_isolation.py +132 -0
- sqlmodel_ext-0.3.3/uv.lock +585 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/.gitignore +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/LICENSE +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/README.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/README_zh.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/.vitepress/config.mts +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/.vitepress/en.mts +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/.vitepress/theme/index.ts +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/.vitepress/theme/style/custom.css +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/.vitepress/zh.mts +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/cached-table.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/crud-pipeline.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/metaclass.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/polymorphic-internals.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/crud.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/field-types.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/pagination.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/cache-queries.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/define-sti-models.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/handle-concurrent-updates.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/integrate-with-fastapi.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/prevent-missing-greenlet.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/how-to/release-connection-during-long-io.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/crud.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/reference/base-classes.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/reference/crud-methods.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/reference/decorators.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/reference/field-types.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/reference/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/reference/mixins.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/reference/pagination-types.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/tutorials/01-getting-started.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/tutorials/02-building-a-blog-api.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/tutorials/03-adding-redis-cache.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/en/tutorials/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/cached-table.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/crud-pipeline.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/metaclass.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/polymorphic-internals.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/crud.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/field-types.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/pagination.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/cache-queries.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/define-sti-models.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/handle-concurrent-updates.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/integrate-with-fastapi.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/paginate-a-list-endpoint.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/prevent-missing-greenlet.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/how-to/release-connection-during-long-io.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/crud.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/reference/base-classes.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/reference/crud-methods.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/reference/decorators.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/reference/field-types.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/reference/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/reference/mixins.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/reference/pagination-types.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/tutorials/01-getting-started.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/tutorials/02-building-a-blog-api.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/tutorials/03-adding-redis-cache.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/docs/tutorials/index.md +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/package.json +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/pnpm-lock.yaml +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/_exceptions.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/_sa_type.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/_utils.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/field_types/url.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/mixins/info_response.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/mixins/polymorphic.py +0 -0
- {sqlmodel_ext-0.3.2 → sqlmodel_ext-0.3.3}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ci-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
# -------------------------------------------------------------------
|
|
19
|
+
# Job 1: source-tree tests across supported Python versions.
|
|
20
|
+
# Uses uv sync so the lockfile is honoured and runs are reproducible.
|
|
21
|
+
# -------------------------------------------------------------------
|
|
22
|
+
test:
|
|
23
|
+
name: test (py${{ matrix.python-version }})
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
strategy:
|
|
26
|
+
fail-fast: false
|
|
27
|
+
matrix:
|
|
28
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
29
|
+
steps:
|
|
30
|
+
- name: Checkout
|
|
31
|
+
uses: actions/checkout@v4
|
|
32
|
+
|
|
33
|
+
- name: Install uv
|
|
34
|
+
uses: astral-sh/setup-uv@v5
|
|
35
|
+
with:
|
|
36
|
+
enable-cache: true
|
|
37
|
+
cache-dependency-glob: "uv.lock"
|
|
38
|
+
|
|
39
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
40
|
+
run: uv python install ${{ matrix.python-version }}
|
|
41
|
+
|
|
42
|
+
- name: Create venv and install project with dev extras
|
|
43
|
+
# Dev extras: pytest, pytest-asyncio, aiosqlite, ruff. Editable install
|
|
44
|
+
# keeps the wheel build separate for the consumer-integration job.
|
|
45
|
+
run: |
|
|
46
|
+
uv venv --python ${{ matrix.python-version }} .venv
|
|
47
|
+
uv pip install --python .venv/bin/python -e ".[dev]"
|
|
48
|
+
|
|
49
|
+
- name: Run pytest
|
|
50
|
+
run: .venv/bin/python -m pytest tests/ -v
|
|
51
|
+
|
|
52
|
+
# -------------------------------------------------------------------
|
|
53
|
+
# Job 2: build wheel + sdist, then install the wheel into a clean
|
|
54
|
+
# virtualenv with zero dev deps and run the consumer integration
|
|
55
|
+
# script. This simulates an external PyPI consumer.
|
|
56
|
+
# -------------------------------------------------------------------
|
|
57
|
+
consumer-integration:
|
|
58
|
+
name: consumer integration (py${{ matrix.python-version }})
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
needs: test
|
|
61
|
+
strategy:
|
|
62
|
+
fail-fast: false
|
|
63
|
+
matrix:
|
|
64
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
65
|
+
steps:
|
|
66
|
+
- name: Checkout
|
|
67
|
+
uses: actions/checkout@v4
|
|
68
|
+
|
|
69
|
+
- name: Install uv
|
|
70
|
+
uses: astral-sh/setup-uv@v5
|
|
71
|
+
with:
|
|
72
|
+
enable-cache: true
|
|
73
|
+
|
|
74
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
75
|
+
run: uv python install ${{ matrix.python-version }}
|
|
76
|
+
|
|
77
|
+
- name: Build wheel and sdist
|
|
78
|
+
run: uv tool run --from build python -m build
|
|
79
|
+
|
|
80
|
+
- name: Create isolated consumer venv
|
|
81
|
+
run: uv venv --python ${{ matrix.python-version }} /tmp/consumer-env
|
|
82
|
+
|
|
83
|
+
- name: Install built wheel (no dev deps, only the wheel + aiosqlite driver)
|
|
84
|
+
run: uv pip install --python /tmp/consumer-env/bin/python dist/*.whl aiosqlite
|
|
85
|
+
|
|
86
|
+
- name: Confirm installed version matches wheel
|
|
87
|
+
run: |
|
|
88
|
+
/tmp/consumer-env/bin/python -c "import sqlmodel_ext; print('installed:', sqlmodel_ext.__version__ if hasattr(sqlmodel_ext, '__version__') else '(no __version__)')"
|
|
89
|
+
|
|
90
|
+
- name: Run consumer integration scenario
|
|
91
|
+
run: /tmp/consumer-env/bin/python examples/consumer_integration/run.py
|
|
92
|
+
|
|
93
|
+
- name: Upload distribution artifacts
|
|
94
|
+
if: matrix.python-version == '3.12'
|
|
95
|
+
uses: actions/upload-artifact@v4
|
|
96
|
+
with:
|
|
97
|
+
name: dist
|
|
98
|
+
path: dist/
|
|
99
|
+
if-no-files-found: error
|
|
100
|
+
retention-days: 14
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-ext
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Foxerine/sqlmodel-ext
|
|
6
6
|
Project-URL: Repository, https://github.com/Foxerine/sqlmodel-ext
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Consumer-side integration test.
|
|
3
|
+
|
|
4
|
+
Runs from a clean virtualenv that has ONLY the built sqlmodel-ext wheel
|
|
5
|
+
installed (plus aiosqlite for the async SQLite driver). Simulates what an
|
|
6
|
+
external PyPI consumer experiences: if this script passes, the wheel is
|
|
7
|
+
well-formed and the STI default-isolation fix behaves end-to-end.
|
|
8
|
+
|
|
9
|
+
Exits with status 0 on success, non-zero on any failed assertion. Intended
|
|
10
|
+
to be invoked directly by CI:
|
|
11
|
+
|
|
12
|
+
python -m pip install dist/*.whl aiosqlite
|
|
13
|
+
python examples/consumer_integration/run.py
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from sqlalchemy import select
|
|
21
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
22
|
+
from sqlalchemy.orm import configure_mappers
|
|
23
|
+
from sqlmodel import SQLModel
|
|
24
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
25
|
+
|
|
26
|
+
from sqlmodel_ext import (
|
|
27
|
+
AutoPolymorphicIdentityMixin,
|
|
28
|
+
PolymorphicBaseMixin,
|
|
29
|
+
SQLModelBase,
|
|
30
|
+
UUIDTableBaseMixin,
|
|
31
|
+
register_sti_column_properties_for_all_subclasses,
|
|
32
|
+
register_sti_columns_for_all_subclasses,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------- STI model hierarchy ----------
|
|
37
|
+
|
|
38
|
+
class Tool(SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin, table=True):
|
|
39
|
+
"""Shared STI parent table."""
|
|
40
|
+
name: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ExportFunction(Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
44
|
+
"""Declares ``max_size`` with a large realistic default."""
|
|
45
|
+
# 5 GiB chosen because the real-world production bug used exactly this value.
|
|
46
|
+
max_size: int = 5 * 1024 * 1024 * 1024
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UploadFunction(Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
50
|
+
"""Declares a different default for a different field."""
|
|
51
|
+
allow_overwrite: bool = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NoOpFunction(Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
55
|
+
"""The sibling with zero own fields - the purest victim of default leaks."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------- Integration scenario ----------
|
|
60
|
+
|
|
61
|
+
FIVE_GIB = 5 * 1024 * 1024 * 1024
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fail(msg: str) -> None:
|
|
65
|
+
print(f"[FAIL] {msg}", file=sys.stderr)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def main() -> None:
|
|
70
|
+
# STI two-phase registration (public API contract).
|
|
71
|
+
register_sti_columns_for_all_subclasses()
|
|
72
|
+
configure_mappers()
|
|
73
|
+
register_sti_column_properties_for_all_subclasses()
|
|
74
|
+
|
|
75
|
+
# --- Layer 1: metadata assertions ---
|
|
76
|
+
shared = Tool.__table__.columns
|
|
77
|
+
if shared["max_size"].default is not None:
|
|
78
|
+
fail(
|
|
79
|
+
f"tool.max_size.default should be None (fix cleared the shared SA "
|
|
80
|
+
f"default). Got: {shared['max_size'].default!r}"
|
|
81
|
+
)
|
|
82
|
+
if shared["max_size"].server_default is not None:
|
|
83
|
+
fail(f"tool.max_size.server_default should be None")
|
|
84
|
+
if shared["allow_overwrite"].default is not None:
|
|
85
|
+
fail(
|
|
86
|
+
f"tool.allow_overwrite.default should be None. "
|
|
87
|
+
f"Got: {shared['allow_overwrite'].default!r}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Pydantic field-level defaults must still be intact.
|
|
91
|
+
if ExportFunction.model_fields["max_size"].default != FIVE_GIB:
|
|
92
|
+
fail(
|
|
93
|
+
f"Pydantic default for ExportFunction.max_size was wiped by the fix: "
|
|
94
|
+
f"{ExportFunction.model_fields['max_size'].default!r}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# --- Layer 2: behavioural assertions via real async SQLite ---
|
|
98
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
99
|
+
try:
|
|
100
|
+
async with engine.begin() as conn:
|
|
101
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
102
|
+
|
|
103
|
+
async with AsyncSession(engine) as session:
|
|
104
|
+
# NoOpFunction does not declare max_size; the shared column must be NULL.
|
|
105
|
+
noop = NoOpFunction(name="noop")
|
|
106
|
+
noop = await noop.save(session)
|
|
107
|
+
result = await session.execute(
|
|
108
|
+
select(Tool.__table__.c.max_size).where(Tool.__table__.c.id == noop.id)
|
|
109
|
+
)
|
|
110
|
+
stored = result.scalar_one()
|
|
111
|
+
if stored is not None:
|
|
112
|
+
fail(
|
|
113
|
+
f"NoOpFunction.max_size should be NULL (bug: sibling default "
|
|
114
|
+
f"leaked from ExportFunction). Got: {stored!r}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# UploadFunction also does not declare max_size.
|
|
118
|
+
upload = UploadFunction(name="upload")
|
|
119
|
+
upload = await upload.save(session)
|
|
120
|
+
result = await session.execute(
|
|
121
|
+
select(Tool.__table__.c.max_size).where(Tool.__table__.c.id == upload.id)
|
|
122
|
+
)
|
|
123
|
+
stored = result.scalar_one()
|
|
124
|
+
if stored is not None:
|
|
125
|
+
fail(
|
|
126
|
+
f"UploadFunction.max_size should be NULL. Got: {stored!r}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Happy path: declaring subclass still persists its own Pydantic default.
|
|
130
|
+
export = ExportFunction(name="export")
|
|
131
|
+
export = await export.save(session)
|
|
132
|
+
result = await session.execute(
|
|
133
|
+
select(Tool.__table__.c.max_size).where(Tool.__table__.c.id == export.id)
|
|
134
|
+
)
|
|
135
|
+
stored = result.scalar_one()
|
|
136
|
+
if stored != FIVE_GIB:
|
|
137
|
+
fail(
|
|
138
|
+
f"ExportFunction.max_size should be {FIVE_GIB}. Got: {stored!r} "
|
|
139
|
+
f"- fix may have broken the declaring subclass's default path."
|
|
140
|
+
)
|
|
141
|
+
finally:
|
|
142
|
+
await engine.dispose()
|
|
143
|
+
|
|
144
|
+
print("[OK] sqlmodel-ext consumer integration test passed")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
asyncio.run(main())
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlmodel-ext"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -67,10 +67,15 @@ if sys.version_info >= (3, 14):
|
|
|
67
67
|
# Check field.metadata (Pydantic-processed Annotated types)
|
|
68
68
|
metadata = getattr(field, 'metadata', None)
|
|
69
69
|
if metadata:
|
|
70
|
+
# Pass field.annotation as source_type so handlers that need the element type
|
|
71
|
+
# (e.g. _PendingArrayHandler extracting T from list[T] to compute ARRAY(T))
|
|
72
|
+
# can resolve. handler=None is fine because the sa_type extraction path only
|
|
73
|
+
# reads metadata['sa_type'] and never invokes the schema itself.
|
|
74
|
+
field_annotation = getattr(field, 'annotation', None)
|
|
70
75
|
for metadata_item in metadata:
|
|
71
76
|
if hasattr(metadata_item, '__get_pydantic_core_schema__'):
|
|
72
77
|
try:
|
|
73
|
-
schema = metadata_item.__get_pydantic_core_schema__(
|
|
78
|
+
schema = metadata_item.__get_pydantic_core_schema__(field_annotation, None)
|
|
74
79
|
if isinstance(schema, dict) and 'metadata' in schema:
|
|
75
80
|
sa_type = schema['metadata'].get('sa_type')
|
|
76
81
|
if sa_type is not None:
|
|
@@ -105,7 +110,9 @@ if sys.version_info >= (3, 14):
|
|
|
105
110
|
for md in inner_args[1:]:
|
|
106
111
|
if hasattr(md, '__get_pydantic_core_schema__'):
|
|
107
112
|
try:
|
|
108
|
-
|
|
113
|
+
# Pass first_arg (the full Annotated alias) as source_type so
|
|
114
|
+
# handlers like _PendingArrayHandler can extract the element type.
|
|
115
|
+
schema = md.__get_pydantic_core_schema__(first_arg, None)
|
|
109
116
|
if isinstance(schema, dict) and 'metadata' in schema:
|
|
110
117
|
sa_type = schema['metadata'].get('sa_type')
|
|
111
118
|
if sa_type is not None:
|
|
@@ -153,7 +160,10 @@ if sys.version_info >= (3, 14):
|
|
|
153
160
|
for md in args[1:]:
|
|
154
161
|
if hasattr(md, '__get_pydantic_core_schema__'):
|
|
155
162
|
try:
|
|
156
|
-
|
|
163
|
+
# Pass annotation (the full Annotated form) as source_type so
|
|
164
|
+
# handlers needing the element type (_PendingArrayHandler etc.)
|
|
165
|
+
# can extract T from list[T].
|
|
166
|
+
schema = md.__get_pydantic_core_schema__(annotation, None)
|
|
157
167
|
if isinstance(schema, dict) and 'metadata' in schema:
|
|
158
168
|
sa_type = schema['metadata'].get('sa_type')
|
|
159
169
|
if sa_type is not None:
|
|
@@ -15,7 +15,7 @@ import re
|
|
|
15
15
|
import sys
|
|
16
16
|
import types
|
|
17
17
|
import typing
|
|
18
|
-
from typing import Any, get_args, get_origin
|
|
18
|
+
from typing import Any, Self, Sequence, get_args, get_origin
|
|
19
19
|
|
|
20
20
|
from pydantic import ConfigDict, model_validator
|
|
21
21
|
from pydantic.fields import FieldInfo
|
|
@@ -102,6 +102,47 @@ def _merge_field_info_attrs(target: SQLModelFieldInfo, source: SQLModelFieldInfo
|
|
|
102
102
|
target.metadata = list(target_meta) + list(source_meta)
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def _find_field_info_in_annotated(annotation: Any) -> SQLModelFieldInfo | None:
|
|
106
|
+
"""
|
|
107
|
+
Extract SQLModel ``FieldInfo`` embedded in ``Annotated[T, Field(...)]`` metadata.
|
|
108
|
+
|
|
109
|
+
Used by the metaclass sa_type injection loop. When a field is declared
|
|
110
|
+
``Annotated[X, Field(default_factory=list, ...)]`` *without* an explicit
|
|
111
|
+
``= ...`` assignment, ``attrs[field_name]`` is ``Undefined``. Replacing
|
|
112
|
+
it with a fresh ``Field(sa_type=sa_type)`` would discard the user's
|
|
113
|
+
Field metadata (``default_factory``, ``max_length``, validators, ...)
|
|
114
|
+
that lives inside the Annotated args. This helper recovers that
|
|
115
|
+
FieldInfo so the caller can attach ``sa_type`` to the user's Field
|
|
116
|
+
instead of clobbering it. The bug only surfaces after 2+ levels of
|
|
117
|
+
inheritance: single-class instantiation goes through Pydantic's native
|
|
118
|
+
Annotated path and works, but child classes rebuild ``model_fields``
|
|
119
|
+
from the clobbered ``attrs`` and the field becomes silently
|
|
120
|
+
``is_required=True``.
|
|
121
|
+
|
|
122
|
+
Multiple FieldInfo args (e.g. ``Annotated[Str64, Field(unique=True)]``
|
|
123
|
+
expands to ``Annotated[str, Field(max_length=64), Field(unique=True)]``)
|
|
124
|
+
are merged into a single shallow copy so the shared Annotated metadata
|
|
125
|
+
singletons are never mutated.
|
|
126
|
+
|
|
127
|
+
:param annotation: Field type annotation
|
|
128
|
+
:returns: Merged SQLModelFieldInfo (shallow copy, safe to mutate), or None
|
|
129
|
+
"""
|
|
130
|
+
if get_origin(annotation) is not typing.Annotated:
|
|
131
|
+
return None
|
|
132
|
+
args = get_args(annotation)
|
|
133
|
+
if len(args) < 2:
|
|
134
|
+
return None
|
|
135
|
+
sqlmodel_fis: list[SQLModelFieldInfo] = [
|
|
136
|
+
arg for arg in args[1:] if isinstance(arg, SQLModelFieldInfo)
|
|
137
|
+
]
|
|
138
|
+
if not sqlmodel_fis:
|
|
139
|
+
return None
|
|
140
|
+
merged = copy.copy(sqlmodel_fis[0])
|
|
141
|
+
for extra_fi in sqlmodel_fis[1:]:
|
|
142
|
+
_merge_field_info_attrs(merged, extra_fi)
|
|
143
|
+
return merged
|
|
144
|
+
|
|
145
|
+
|
|
105
146
|
def _make_annotation_optional(annotation: typing.Any) -> typing.Any:
|
|
106
147
|
"""
|
|
107
148
|
Convert type annotation to optional: ``T → T | None``
|
|
@@ -179,6 +220,27 @@ def _apply_all_fields_optional(
|
|
|
179
220
|
if original_ann is None:
|
|
180
221
|
continue
|
|
181
222
|
|
|
223
|
+
# Bug fix: when a field is declared as ``field: T = Field(gt=..., le=...)`` (non-Annotated form),
|
|
224
|
+
# MRO ``__annotations__`` only stores the bare ``T``. The ``Field(...)`` constraint metadata
|
|
225
|
+
# lives on the right-hand-side assignment, so all_fields_optional-derived UpdateRequest classes
|
|
226
|
+
# lose those constraints (e.g. UFT-style "missing range" warnings fire on derived fields that
|
|
227
|
+
# had constraints in the source class). Patch: when ``original_ann`` is not already Annotated,
|
|
228
|
+
# pull constraints from the base class's resolved ``model_fields[name].metadata`` (Pydantic
|
|
229
|
+
# stores them there as ``[Gt(0), Le(600), ...]``) and re-wrap into ``Annotated[T, *metadata]``,
|
|
230
|
+
# letting downstream ``_make_annotation_optional`` preserve the constraints when converting
|
|
231
|
+
# to ``T | None``. The root recommended fix is still to declare fields in Annotated form at
|
|
232
|
+
# the source; this patch is defense-in-depth for legacy ``field: T = Field(...)`` declarations.
|
|
233
|
+
if get_origin(original_ann) is not typing.Annotated:
|
|
234
|
+
for base in bases:
|
|
235
|
+
base_model_fields = getattr(base, 'model_fields', None)
|
|
236
|
+
if not base_model_fields:
|
|
237
|
+
continue
|
|
238
|
+
base_field_info = base_model_fields.get(field_name)
|
|
239
|
+
if base_field_info is None or not base_field_info.metadata:
|
|
240
|
+
continue
|
|
241
|
+
original_ann = typing.Annotated[original_ann, *base_field_info.metadata]
|
|
242
|
+
break
|
|
243
|
+
|
|
182
244
|
# Skip Literal type fields (e.g. discriminator):
|
|
183
245
|
# Literal['text'] | None breaks Pydantic discriminated union
|
|
184
246
|
raw_type = original_ann
|
|
@@ -268,10 +330,33 @@ def _recover_annotated_sqlmodel_fields(
|
|
|
268
330
|
all_annotated.update(annotations)
|
|
269
331
|
|
|
270
332
|
for field_name, field_type in all_annotated.items():
|
|
271
|
-
|
|
333
|
+
# Unwrap Optional/Union wrappers: forms like ``Annotated[X, Field(sa_type=BigInteger)] | None``
|
|
334
|
+
# have ``get_origin() == Union`` (or ``types.UnionType`` for PEP 604 ``|``); we must
|
|
335
|
+
# peel back to find the inner Annotated to extract the SQLModel FieldInfo. Otherwise
|
|
336
|
+
# nullable aliases like ``PositiveBigInt | None`` lose ``sa_type`` and the SA column
|
|
337
|
+
# silently degrades (e.g. BigInteger → Integer → asyncpg int32 overflow on large defaults).
|
|
338
|
+
annotated_type = field_type
|
|
339
|
+
union_args: list[typing.Any] | None = None
|
|
340
|
+
union_origin = get_origin(field_type)
|
|
341
|
+
# ``X | None`` (PEP 604) has origin ``types.UnionType``; ``Union[X, None]`` is ``typing.Union``.
|
|
342
|
+
# Match both without depending on ``types`` import inside any version block.
|
|
343
|
+
is_union = union_origin is typing.Union or (
|
|
344
|
+
union_origin is not None and getattr(union_origin, '__name__', '') == 'UnionType'
|
|
345
|
+
)
|
|
346
|
+
if is_union:
|
|
347
|
+
union_args = list(get_args(field_type))
|
|
348
|
+
annotated_in_union = next(
|
|
349
|
+
(arg for arg in union_args if get_origin(arg) is typing.Annotated),
|
|
350
|
+
None,
|
|
351
|
+
)
|
|
352
|
+
if annotated_in_union is None:
|
|
353
|
+
continue
|
|
354
|
+
annotated_type = annotated_in_union
|
|
355
|
+
|
|
356
|
+
if get_origin(annotated_type) is not typing.Annotated:
|
|
272
357
|
continue
|
|
273
358
|
|
|
274
|
-
args = get_args(
|
|
359
|
+
args = get_args(annotated_type)
|
|
275
360
|
if len(args) < 2:
|
|
276
361
|
continue
|
|
277
362
|
|
|
@@ -313,10 +398,24 @@ def _recover_annotated_sqlmodel_fields(
|
|
|
313
398
|
base_type = args[0]
|
|
314
399
|
remaining_metadata = [a for a in args[1:] if not isinstance(a, SQLModelFieldInfo)]
|
|
315
400
|
if remaining_metadata:
|
|
316
|
-
|
|
401
|
+
new_inner: typing.Any = typing.Annotated[tuple([base_type] + remaining_metadata)]
|
|
317
402
|
else:
|
|
318
|
-
|
|
319
|
-
|
|
403
|
+
new_inner = base_type
|
|
404
|
+
|
|
405
|
+
if union_args is not None:
|
|
406
|
+
# Rebuild Union: replace the inner Annotated with the stripped version, keeping
|
|
407
|
+
# the other Union members (e.g. ``| None``) intact. Use PEP 604 ``|`` to accumulate
|
|
408
|
+
# so we don't trip the deprecated-typing.Union warning.
|
|
409
|
+
new_union_args = [
|
|
410
|
+
new_inner if get_origin(arg) is typing.Annotated else arg
|
|
411
|
+
for arg in union_args
|
|
412
|
+
]
|
|
413
|
+
rebuilt: typing.Any = new_union_args[0]
|
|
414
|
+
for extra in new_union_args[1:]:
|
|
415
|
+
rebuilt = rebuilt | extra
|
|
416
|
+
annotations[field_name] = rebuilt
|
|
417
|
+
else:
|
|
418
|
+
annotations[field_name] = new_inner
|
|
320
419
|
|
|
321
420
|
|
|
322
421
|
def _make_sti_fk_resolver(
|
|
@@ -495,7 +594,19 @@ class __DeclarativeMeta(SQLModelMetaclass):
|
|
|
495
594
|
field_value = attrs.get(field_name, Undefined)
|
|
496
595
|
|
|
497
596
|
if field_value is Undefined:
|
|
498
|
-
|
|
597
|
+
# No explicit ``= Field(...)`` assignment. Prefer recovering
|
|
598
|
+
# FieldInfo from inside Annotated[X, Field(default_factory=..., ...)]
|
|
599
|
+
# so user-supplied default_factory / max_length / constraints
|
|
600
|
+
# survive — clobbering with a fresh Field(sa_type=sa_type)
|
|
601
|
+
# would silently make the field required after multi-level
|
|
602
|
+
# inheritance.
|
|
603
|
+
annotated_fi = _find_field_info_in_annotated(field_type)
|
|
604
|
+
if annotated_fi is not None:
|
|
605
|
+
if getattr(annotated_fi, 'sa_type', Undefined) is Undefined:
|
|
606
|
+
setattr(annotated_fi, 'sa_type', sa_type)
|
|
607
|
+
attrs[field_name] = annotated_fi
|
|
608
|
+
else:
|
|
609
|
+
attrs[field_name] = Field(sa_type=sa_type)
|
|
499
610
|
elif isinstance(field_value, FieldInfo):
|
|
500
611
|
if getattr(field_value, 'sa_type', Undefined) is Undefined:
|
|
501
612
|
setattr(field_value, 'sa_type', sa_type)
|
|
@@ -975,6 +1086,11 @@ class SQLModelBase(SQLModel, metaclass=__DeclarativeMeta):
|
|
|
975
1086
|
prop['description'] = finfo.description
|
|
976
1087
|
return json_schema
|
|
977
1088
|
|
|
1089
|
+
@classmethod
|
|
1090
|
+
def validate_list(cls, items: Sequence[Any]) -> list[Self]:
|
|
1091
|
+
"""Batch-convert a sequence of ORM instances (or dicts) to this model type."""
|
|
1092
|
+
return [cls.model_validate(item, from_attributes=True) for item in items]
|
|
1093
|
+
|
|
978
1094
|
@classmethod
|
|
979
1095
|
def get_computed_field_names(cls) -> set[str]:
|
|
980
1096
|
"""Get the set of computed_field names for this model class."""
|
|
@@ -108,6 +108,9 @@ Text5K: TypeAlias = Annotated[str, Field(max_length=5000), _NO_NULL_BYTE]
|
|
|
108
108
|
Text10K: TypeAlias = Annotated[str, Field(max_length=10000), _NO_NULL_BYTE]
|
|
109
109
|
"""10000-character text field"""
|
|
110
110
|
|
|
111
|
+
Text16K: TypeAlias = Annotated[str, Field(max_length=16000), _NO_NULL_BYTE]
|
|
112
|
+
"""16000-character text field (long-form descriptions, tool docs)"""
|
|
113
|
+
|
|
111
114
|
Text32K: TypeAlias = Annotated[str, Field(max_length=32000), _NO_NULL_BYTE]
|
|
112
115
|
"""32000-character text field"""
|
|
113
116
|
|
|
@@ -120,9 +123,29 @@ Text64K: TypeAlias = Annotated[str, Field(max_length=65536), _NO_NULL_BYTE]
|
|
|
120
123
|
Text100K: TypeAlias = Annotated[str, Field(max_length=100000), _NO_NULL_BYTE]
|
|
121
124
|
"""100000-character text field"""
|
|
122
125
|
|
|
126
|
+
Text128K: TypeAlias = Annotated[str, Field(max_length=131072), _NO_NULL_BYTE]
|
|
127
|
+
"""131072-character (128 * 1024) text field (large markdown documents etc.)"""
|
|
128
|
+
|
|
123
129
|
Text1M: TypeAlias = Annotated[str, Field(max_length=1000000), _NO_NULL_BYTE]
|
|
124
130
|
"""1000000-character text field (tool call parameters, tool responses, etc.)"""
|
|
125
131
|
|
|
132
|
+
# NonEmptyStr* — same as Str* but also requires ``min_length=1``, rejecting empty
|
|
133
|
+
# ``""`` strings with a 422 ValidationError. Use for naming fields where the empty
|
|
134
|
+
# string is semantically invalid (e.g. UserFolder.name).
|
|
135
|
+
NonEmptyStr64: TypeAlias = Annotated[str, Field(min_length=1, max_length=64), _NO_NULL_BYTE]
|
|
136
|
+
"""1-64 character non-empty string field"""
|
|
137
|
+
|
|
138
|
+
NonEmptyStr128: TypeAlias = Annotated[str, Field(min_length=1, max_length=128), _NO_NULL_BYTE]
|
|
139
|
+
"""1-128 character non-empty string field"""
|
|
140
|
+
|
|
141
|
+
NonEmptyStr256: TypeAlias = Annotated[str, Field(min_length=1, max_length=256), _NO_NULL_BYTE]
|
|
142
|
+
"""1-256 character non-empty string field"""
|
|
143
|
+
|
|
144
|
+
# Sha256Hex — exactly 64 lowercase hex characters, the canonical SHA-256 hex digest form.
|
|
145
|
+
# The strict regex also implicitly rejects NUL bytes, so no separate _NO_NULL_BYTE is needed.
|
|
146
|
+
Sha256Hex: TypeAlias = Annotated[str, StringConstraints(min_length=64, max_length=64, pattern=r'^[0-9a-f]{64}$')]
|
|
147
|
+
"""64-char lowercase-hex SHA-256 digest (e.g. content hashes)"""
|
|
148
|
+
|
|
126
149
|
# Numeric range constraints
|
|
127
150
|
Port: TypeAlias = Annotated[int, Field(ge=1, le=65535)]
|
|
128
151
|
"""Port number (1-65535)"""
|