ferro-orm 0.2.1__tar.gz → 0.3.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.
- ferro_orm-0.3.0/.github/workflows/release.yml +491 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.gitignore +2 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/CHANGELOG.md +54 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/Cargo.lock +1 -1
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/Cargo.toml +1 -1
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/PKG-INFO +1 -1
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/concepts/architecture.md +1 -1
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/guide/migrations.md +4 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/guide/models-and-fields.md +36 -2
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/guide/relationships.md +12 -3
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/pyproject.toml +3 -2
- ferro_orm-0.3.0/src/ferro/_shadow_fk_types.py +137 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/base.py +5 -2
- ferro_orm-0.3.0/src/ferro/composite_uniques.py +97 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/fields.py +2 -1
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/metaclass.py +15 -4
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/migrations/alembic.py +23 -1
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/models.py +10 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/relations/__init__.py +14 -2
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/schema.rs +51 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_alembic_bridge.py +22 -0
- ferro_orm-0.3.0/tests/test_composite_unique.py +324 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_metaclass_internals.py +20 -1
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_relationship_engine.py +2 -0
- ferro_orm-0.3.0/tests/test_shadow_fk_types.py +258 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/uv.lock +1 -1
- ferro_orm-0.2.1/.github/workflows/release.yml +0 -320
- ferro_orm-0.2.1/DOCS_COMPLETE.md +0 -226
- ferro_orm-0.2.1/DOCS_RESTRUCTURE_SUMMARY.md +0 -213
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/.python-version +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/LICENSE +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/README.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/TEST_RESULTS.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/api/fields.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/api/model.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/api/query.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/api/relationships.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/api/transactions.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/api/utilities.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/changelog.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/coming-soon.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/concepts/identity-map.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/concepts/performance.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/concepts/type-safety.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/contributing.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/faq.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/getting-started/installation.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/getting-started/next-steps.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/getting-started/tutorial.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/guide/database.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/guide/mutations.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/guide/queries.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/guide/transactions.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/howto/multiple-databases.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/howto/pagination.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/howto/soft-deletes.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/howto/testing.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/howto/timestamps.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/index.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/migration-sqlalchemy.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/stylesheets/extra.css +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/docs/why-ferro.md +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/justfile +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/mkdocs.yml +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/connection.rs +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/__init__.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/py.typed +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/query/builder.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/ferro/state.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/lib.rs +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/operations.rs +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/query.rs +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/src/state.rs +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/conftest.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_connection.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_constraints.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_crud.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_deletion.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_helpers.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_hydration.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_metadata.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_models.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_refresh.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_schema.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_string_search.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.2.1 → ferro_orm-0.3.0}/tests/test_transactions.py +0 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
prerelease:
|
|
7
|
+
description: 'Create a pre-release'
|
|
8
|
+
required: false
|
|
9
|
+
type: boolean
|
|
10
|
+
default: false
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: release-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
ci-gate:
|
|
18
|
+
name: CI Gate
|
|
19
|
+
uses: ./.github/workflows/ci.yml
|
|
20
|
+
|
|
21
|
+
packaging-smoke:
|
|
22
|
+
name: Packaging Smoke Gate
|
|
23
|
+
uses: ./.github/workflows/packaging-smoke.yml
|
|
24
|
+
|
|
25
|
+
# Bump versions and changelog locally only; export a patch so every later job
|
|
26
|
+
# validates the exact tree that would be released. Nothing is pushed until
|
|
27
|
+
# publish-github-release after wheels, sdist, tests, and docs all pass.
|
|
28
|
+
prepare-release:
|
|
29
|
+
name: Prepare release (no push)
|
|
30
|
+
needs: [ci-gate, packaging-smoke]
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
outputs:
|
|
33
|
+
release_created: ${{ steps.verify_release.outputs.release_created }}
|
|
34
|
+
release_base_sha: ${{ steps.base_sha.outputs.release_base_sha }}
|
|
35
|
+
release_tag: ${{ steps.verify_release.outputs.release_tag }}
|
|
36
|
+
release_version: ${{ steps.verify_release.outputs.release_version }}
|
|
37
|
+
permissions:
|
|
38
|
+
contents: read
|
|
39
|
+
issues: read
|
|
40
|
+
pull-requests: read
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- name: Record workflow base SHA
|
|
44
|
+
id: base_sha
|
|
45
|
+
run: echo "release_base_sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
|
46
|
+
|
|
47
|
+
- name: Checkout repository
|
|
48
|
+
uses: actions/checkout@v4
|
|
49
|
+
with:
|
|
50
|
+
fetch-depth: 0
|
|
51
|
+
|
|
52
|
+
- name: Set up Python
|
|
53
|
+
uses: actions/setup-python@v5
|
|
54
|
+
with:
|
|
55
|
+
python-version: '3.13'
|
|
56
|
+
|
|
57
|
+
- name: Install UV
|
|
58
|
+
uses: astral-sh/setup-uv@v5
|
|
59
|
+
with:
|
|
60
|
+
enable-cache: true
|
|
61
|
+
|
|
62
|
+
- name: Install dependencies
|
|
63
|
+
run: |
|
|
64
|
+
uv sync --only-group release --no-install-project --python 3.13
|
|
65
|
+
|
|
66
|
+
- name: Run semantic-release (local bump only)
|
|
67
|
+
env:
|
|
68
|
+
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
69
|
+
run: |
|
|
70
|
+
PRERELEASE_FLAG=""
|
|
71
|
+
if [ "${{ github.event.inputs.prerelease }}" == "true" ]; then
|
|
72
|
+
PRERELEASE_FLAG="--prerelease"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Writes version files + CHANGELOG only; no commit, push, tag, or VCS
|
|
76
|
+
# release. --skip-build defers packaging to validation jobs.
|
|
77
|
+
uv run semantic-release version $PRERELEASE_FLAG \
|
|
78
|
+
--no-vcs-release --no-tag --no-commit --no-push --skip-build
|
|
79
|
+
|
|
80
|
+
- name: Verify release candidate and write patch
|
|
81
|
+
id: verify_release
|
|
82
|
+
shell: bash
|
|
83
|
+
run: |
|
|
84
|
+
set -euo pipefail
|
|
85
|
+
# Normalize to a single index-vs-HEAD patch (stable for git apply on runners).
|
|
86
|
+
git add -A
|
|
87
|
+
git diff --cached HEAD > release.patch
|
|
88
|
+
|
|
89
|
+
if [[ ! -s release.patch ]]; then
|
|
90
|
+
echo "No semantic version bump in this run (index matches HEAD)."
|
|
91
|
+
echo "release_created=false" >> "$GITHUB_OUTPUT"
|
|
92
|
+
echo "changelog_validated=skipped" >> "$GITHUB_OUTPUT"
|
|
93
|
+
echo "release_tag=" >> "$GITHUB_OUTPUT"
|
|
94
|
+
echo "release_version=" >> "$GITHUB_OUTPUT"
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if ! grep -qE '^diff --git a/CHANGELOG\.md b/CHANGELOG\.md' release.patch; then
|
|
99
|
+
echo "WARNING: expected CHANGELOG.md to change for a release bump."
|
|
100
|
+
echo "release_created=false" >> "$GITHUB_OUTPUT"
|
|
101
|
+
echo "changelog_validated=missing" >> "$GITHUB_OUTPUT"
|
|
102
|
+
echo "release_tag=" >> "$GITHUB_OUTPUT"
|
|
103
|
+
echo "release_version=" >> "$GITHUB_OUTPUT"
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
VERSION="$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")"
|
|
108
|
+
echo "release_created=true" >> "$GITHUB_OUTPUT"
|
|
109
|
+
echo "changelog_validated=passed" >> "$GITHUB_OUTPUT"
|
|
110
|
+
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
111
|
+
echo "release_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
112
|
+
echo "Prepared v${VERSION} as patch against ${{ github.sha }}"
|
|
113
|
+
|
|
114
|
+
- name: Upload release patch
|
|
115
|
+
if: steps.verify_release.outputs.release_created == 'true'
|
|
116
|
+
uses: actions/upload-artifact@v4
|
|
117
|
+
with:
|
|
118
|
+
name: release-patch
|
|
119
|
+
path: release.patch
|
|
120
|
+
|
|
121
|
+
- name: Release summary
|
|
122
|
+
if: always()
|
|
123
|
+
shell: bash
|
|
124
|
+
run: |
|
|
125
|
+
{
|
|
126
|
+
echo "## Release workflow summary"
|
|
127
|
+
echo ""
|
|
128
|
+
echo "- Trigger: \`${{ github.event_name }}\`"
|
|
129
|
+
echo "- Base ref: \`${{ github.ref_name }}\` @ \`${{ github.sha }}\`"
|
|
130
|
+
echo "- Release candidate prepared: \`${{ steps.verify_release.outputs.release_created || 'unknown' }}\`"
|
|
131
|
+
echo "- Changelog validation: \`${{ steps.verify_release.outputs.changelog_validated || 'unknown' }}\`"
|
|
132
|
+
echo "- Tag if published: \`${{ steps.verify_release.outputs.release_tag || 'n/a' }}\`"
|
|
133
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
134
|
+
|
|
135
|
+
# Build and publish jobs are inlined here (not in a reusable workflow) so that
|
|
136
|
+
# PyPI Trusted Publishing sees workflow_ref = release.yml. Reusable workflows
|
|
137
|
+
# are not supported by PyPI Trusted Publishing.
|
|
138
|
+
build-wheels:
|
|
139
|
+
name: Build wheels (${{ matrix.platform.name }})
|
|
140
|
+
needs: [prepare-release]
|
|
141
|
+
if: needs.prepare-release.outputs.release_created == 'true'
|
|
142
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
143
|
+
strategy:
|
|
144
|
+
fail-fast: false
|
|
145
|
+
matrix:
|
|
146
|
+
platform:
|
|
147
|
+
- name: linux-x86_64
|
|
148
|
+
runner: ubuntu-latest
|
|
149
|
+
target: x86_64
|
|
150
|
+
manylinux: auto
|
|
151
|
+
- name: linux-aarch64
|
|
152
|
+
runner: ubuntu-latest
|
|
153
|
+
target: aarch64
|
|
154
|
+
manylinux: auto
|
|
155
|
+
- name: macos-aarch64
|
|
156
|
+
runner: macos-latest
|
|
157
|
+
target: aarch64
|
|
158
|
+
manylinux: ''
|
|
159
|
+
- name: windows-x64
|
|
160
|
+
runner: windows-latest
|
|
161
|
+
target: x64
|
|
162
|
+
manylinux: ''
|
|
163
|
+
|
|
164
|
+
steps:
|
|
165
|
+
- name: Disable Git CRLF conversion before checkout
|
|
166
|
+
shell: bash
|
|
167
|
+
run: git config --global core.autocrlf false
|
|
168
|
+
- name: Checkout repository
|
|
169
|
+
uses: actions/checkout@v4
|
|
170
|
+
with:
|
|
171
|
+
ref: ${{ needs.prepare-release.outputs.release_base_sha }}
|
|
172
|
+
|
|
173
|
+
- name: Download release patch
|
|
174
|
+
uses: actions/download-artifact@v4
|
|
175
|
+
with:
|
|
176
|
+
name: release-patch
|
|
177
|
+
path: release-patch
|
|
178
|
+
|
|
179
|
+
- name: Apply release patch
|
|
180
|
+
shell: bash
|
|
181
|
+
run: |
|
|
182
|
+
git config core.autocrlf false
|
|
183
|
+
git apply release-patch/release.patch
|
|
184
|
+
rm -rf release-patch
|
|
185
|
+
|
|
186
|
+
- name: Set up Python
|
|
187
|
+
uses: actions/setup-python@v5
|
|
188
|
+
with:
|
|
189
|
+
python-version: '3.13'
|
|
190
|
+
|
|
191
|
+
- name: Build wheels
|
|
192
|
+
uses: PyO3/maturin-action@v1.48.0
|
|
193
|
+
with:
|
|
194
|
+
target: ${{ matrix.platform.target }}
|
|
195
|
+
args: --release --out dist --find-interpreter
|
|
196
|
+
manylinux: ${{ matrix.platform.manylinux }}
|
|
197
|
+
sccache: 'true'
|
|
198
|
+
|
|
199
|
+
- name: Upload wheels
|
|
200
|
+
uses: actions/upload-artifact@v4
|
|
201
|
+
with:
|
|
202
|
+
name: wheels-${{ matrix.platform.name }}
|
|
203
|
+
path: dist/*.whl
|
|
204
|
+
|
|
205
|
+
build-sdist:
|
|
206
|
+
name: Build sdist
|
|
207
|
+
needs: [prepare-release]
|
|
208
|
+
if: needs.prepare-release.outputs.release_created == 'true'
|
|
209
|
+
runs-on: ubuntu-latest
|
|
210
|
+
steps:
|
|
211
|
+
- name: Disable Git CRLF conversion before checkout
|
|
212
|
+
shell: bash
|
|
213
|
+
run: git config --global core.autocrlf false
|
|
214
|
+
- name: Checkout repository
|
|
215
|
+
uses: actions/checkout@v4
|
|
216
|
+
with:
|
|
217
|
+
ref: ${{ needs.prepare-release.outputs.release_base_sha }}
|
|
218
|
+
|
|
219
|
+
- name: Download release patch
|
|
220
|
+
uses: actions/download-artifact@v4
|
|
221
|
+
with:
|
|
222
|
+
name: release-patch
|
|
223
|
+
path: release-patch
|
|
224
|
+
|
|
225
|
+
- name: Apply release patch
|
|
226
|
+
shell: bash
|
|
227
|
+
run: |
|
|
228
|
+
git config core.autocrlf false
|
|
229
|
+
git apply release-patch/release.patch
|
|
230
|
+
rm -rf release-patch
|
|
231
|
+
|
|
232
|
+
- name: Build sdist
|
|
233
|
+
uses: PyO3/maturin-action@v1.48.0
|
|
234
|
+
with:
|
|
235
|
+
command: sdist
|
|
236
|
+
args: --out dist
|
|
237
|
+
|
|
238
|
+
- name: Upload sdist
|
|
239
|
+
uses: actions/upload-artifact@v4
|
|
240
|
+
with:
|
|
241
|
+
name: sdist
|
|
242
|
+
path: dist/*.tar.gz
|
|
243
|
+
|
|
244
|
+
test-wheels:
|
|
245
|
+
name: Test wheels (${{ matrix.os }})
|
|
246
|
+
needs: [prepare-release, build-wheels]
|
|
247
|
+
if: needs.prepare-release.outputs.release_created == 'true'
|
|
248
|
+
runs-on: ${{ matrix.os }}
|
|
249
|
+
strategy:
|
|
250
|
+
fail-fast: false
|
|
251
|
+
matrix:
|
|
252
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
253
|
+
steps:
|
|
254
|
+
- name: Disable Git CRLF conversion before checkout
|
|
255
|
+
shell: bash
|
|
256
|
+
run: git config --global core.autocrlf false
|
|
257
|
+
- name: Checkout repository
|
|
258
|
+
uses: actions/checkout@v4
|
|
259
|
+
with:
|
|
260
|
+
ref: ${{ needs.prepare-release.outputs.release_base_sha }}
|
|
261
|
+
|
|
262
|
+
- name: Download release patch
|
|
263
|
+
uses: actions/download-artifact@v4
|
|
264
|
+
with:
|
|
265
|
+
name: release-patch
|
|
266
|
+
path: release-patch
|
|
267
|
+
|
|
268
|
+
- name: Apply release patch
|
|
269
|
+
shell: bash
|
|
270
|
+
run: |
|
|
271
|
+
git config core.autocrlf false
|
|
272
|
+
git apply release-patch/release.patch
|
|
273
|
+
rm -rf release-patch
|
|
274
|
+
|
|
275
|
+
- name: Set up Python
|
|
276
|
+
uses: actions/setup-python@v5
|
|
277
|
+
with:
|
|
278
|
+
python-version: '3.13'
|
|
279
|
+
|
|
280
|
+
- name: Download wheels
|
|
281
|
+
uses: actions/download-artifact@v4
|
|
282
|
+
with:
|
|
283
|
+
path: dist
|
|
284
|
+
pattern: wheels-*
|
|
285
|
+
merge-multiple: true
|
|
286
|
+
|
|
287
|
+
- name: Install wheel
|
|
288
|
+
shell: bash
|
|
289
|
+
run: |
|
|
290
|
+
python -m pip install --upgrade pip
|
|
291
|
+
python -m pip install --find-links dist ferro-orm
|
|
292
|
+
|
|
293
|
+
- name: Test import
|
|
294
|
+
shell: bash
|
|
295
|
+
run: |
|
|
296
|
+
python -c "import ferro; print('Ferro imported successfully')"
|
|
297
|
+
|
|
298
|
+
- name: Run basic smoke test
|
|
299
|
+
shell: bash
|
|
300
|
+
run: |
|
|
301
|
+
python -c "
|
|
302
|
+
import asyncio
|
|
303
|
+
from ferro import Model, FerroField, connect
|
|
304
|
+
from typing import Annotated
|
|
305
|
+
|
|
306
|
+
class TestModel(Model):
|
|
307
|
+
id: Annotated[int, FerroField(primary_key=True)]
|
|
308
|
+
name: str
|
|
309
|
+
|
|
310
|
+
async def test():
|
|
311
|
+
await connect('sqlite::memory:')
|
|
312
|
+
print('Connection test passed')
|
|
313
|
+
|
|
314
|
+
asyncio.run(test())
|
|
315
|
+
"
|
|
316
|
+
|
|
317
|
+
verify-docs:
|
|
318
|
+
name: Verify documentation build
|
|
319
|
+
needs: [prepare-release]
|
|
320
|
+
if: needs.prepare-release.outputs.release_created == 'true'
|
|
321
|
+
runs-on: ubuntu-latest
|
|
322
|
+
steps:
|
|
323
|
+
- name: Disable Git CRLF conversion before checkout
|
|
324
|
+
shell: bash
|
|
325
|
+
run: git config --global core.autocrlf false
|
|
326
|
+
- name: Checkout repository
|
|
327
|
+
uses: actions/checkout@v4
|
|
328
|
+
with:
|
|
329
|
+
ref: ${{ needs.prepare-release.outputs.release_base_sha }}
|
|
330
|
+
|
|
331
|
+
- name: Download release patch
|
|
332
|
+
uses: actions/download-artifact@v4
|
|
333
|
+
with:
|
|
334
|
+
name: release-patch
|
|
335
|
+
path: release-patch
|
|
336
|
+
|
|
337
|
+
- name: Apply release patch
|
|
338
|
+
shell: bash
|
|
339
|
+
run: |
|
|
340
|
+
git config core.autocrlf false
|
|
341
|
+
git apply release-patch/release.patch
|
|
342
|
+
rm -rf release-patch
|
|
343
|
+
|
|
344
|
+
- name: Set up Python
|
|
345
|
+
uses: actions/setup-python@v5
|
|
346
|
+
with:
|
|
347
|
+
python-version: '3.13'
|
|
348
|
+
|
|
349
|
+
- name: Install UV
|
|
350
|
+
uses: astral-sh/setup-uv@v5
|
|
351
|
+
with:
|
|
352
|
+
enable-cache: true
|
|
353
|
+
|
|
354
|
+
- name: Install documentation dependencies
|
|
355
|
+
run: |
|
|
356
|
+
uv sync --only-group docs --no-install-project --python 3.13
|
|
357
|
+
|
|
358
|
+
- name: Build MkDocs site
|
|
359
|
+
run: |
|
|
360
|
+
uv run --no-sync mkdocs build
|
|
361
|
+
|
|
362
|
+
publish-github-release:
|
|
363
|
+
name: Commit, tag, and create GitHub Release
|
|
364
|
+
needs: [prepare-release, build-wheels, build-sdist, test-wheels, verify-docs]
|
|
365
|
+
if: needs.prepare-release.outputs.release_created == 'true'
|
|
366
|
+
runs-on: ubuntu-latest
|
|
367
|
+
permissions:
|
|
368
|
+
contents: write
|
|
369
|
+
issues: write
|
|
370
|
+
pull-requests: write
|
|
371
|
+
steps:
|
|
372
|
+
- name: Disable Git CRLF conversion before checkout
|
|
373
|
+
shell: bash
|
|
374
|
+
run: git config --global core.autocrlf false
|
|
375
|
+
- name: Checkout repository
|
|
376
|
+
uses: actions/checkout@v4
|
|
377
|
+
with:
|
|
378
|
+
ref: ${{ needs.prepare-release.outputs.release_base_sha }}
|
|
379
|
+
fetch-depth: 0
|
|
380
|
+
ssh-key: ${{ secrets.RELEASE_DEPLOY_KEY }}
|
|
381
|
+
# Required so the deploy key stays loaded for git push in the next step.
|
|
382
|
+
persist-credentials: true
|
|
383
|
+
|
|
384
|
+
- name: Download release patch
|
|
385
|
+
uses: actions/download-artifact@v4
|
|
386
|
+
with:
|
|
387
|
+
name: release-patch
|
|
388
|
+
path: release-patch
|
|
389
|
+
|
|
390
|
+
- name: Apply release patch and push release commit
|
|
391
|
+
env:
|
|
392
|
+
VERSION: ${{ needs.prepare-release.outputs.release_version }}
|
|
393
|
+
TAG: ${{ needs.prepare-release.outputs.release_tag }}
|
|
394
|
+
run: |
|
|
395
|
+
set -euo pipefail
|
|
396
|
+
git config core.autocrlf false
|
|
397
|
+
git apply release-patch/release.patch
|
|
398
|
+
rm -rf release-patch
|
|
399
|
+
|
|
400
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
401
|
+
git config user.name "github-actions[bot]"
|
|
402
|
+
git remote set-url origin "git@github.com:${{ github.repository }}.git"
|
|
403
|
+
|
|
404
|
+
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
|
|
405
|
+
echo "ERROR: tag ${TAG} already exists on the remote."
|
|
406
|
+
exit 1
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
git add -A
|
|
410
|
+
git commit --author="semantic-release <semantic-release@github.com>" \
|
|
411
|
+
-m "chore(release): ${VERSION}" \
|
|
412
|
+
-m "Automatically generated by python-semantic-release"
|
|
413
|
+
git push origin "HEAD:refs/heads/${{ github.ref_name }}"
|
|
414
|
+
|
|
415
|
+
git tag "${TAG}"
|
|
416
|
+
git push origin "refs/tags/${TAG}"
|
|
417
|
+
|
|
418
|
+
# semantic-release matches branch config (e.g. branches.main); detached HEAD fails with
|
|
419
|
+
# "Detached HEAD state cannot match any release groups".
|
|
420
|
+
- name: Attach HEAD to branch for semantic-release
|
|
421
|
+
run: git switch -C "${{ github.ref_name }}"
|
|
422
|
+
|
|
423
|
+
- name: Set up Python
|
|
424
|
+
uses: actions/setup-python@v5
|
|
425
|
+
with:
|
|
426
|
+
python-version: '3.13'
|
|
427
|
+
|
|
428
|
+
- name: Install UV
|
|
429
|
+
uses: astral-sh/setup-uv@v5
|
|
430
|
+
with:
|
|
431
|
+
enable-cache: true
|
|
432
|
+
|
|
433
|
+
- name: Set up Rust
|
|
434
|
+
uses: dtolnay/rust-toolchain@stable
|
|
435
|
+
|
|
436
|
+
- name: Cache Rust dependencies
|
|
437
|
+
uses: Swatinem/rust-cache@v2
|
|
438
|
+
with:
|
|
439
|
+
prefix-key: "v1-rust"
|
|
440
|
+
|
|
441
|
+
- name: Install dependencies
|
|
442
|
+
run: |
|
|
443
|
+
uv sync --only-group release --no-install-project --python 3.13
|
|
444
|
+
|
|
445
|
+
- name: Publish GitHub release (assets + VCS release)
|
|
446
|
+
env:
|
|
447
|
+
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
448
|
+
run: |
|
|
449
|
+
uv run semantic-release publish --tag "${{ needs.prepare-release.outputs.release_tag }}"
|
|
450
|
+
|
|
451
|
+
publish-pypi:
|
|
452
|
+
name: Publish to PyPI
|
|
453
|
+
needs: [prepare-release, publish-github-release]
|
|
454
|
+
if: needs.prepare-release.outputs.release_created == 'true'
|
|
455
|
+
runs-on: ubuntu-latest
|
|
456
|
+
environment:
|
|
457
|
+
name: pypi
|
|
458
|
+
url: https://pypi.org/p/ferro-orm
|
|
459
|
+
permissions:
|
|
460
|
+
id-token: write
|
|
461
|
+
steps:
|
|
462
|
+
- name: Download sdist
|
|
463
|
+
uses: actions/download-artifact@v4
|
|
464
|
+
with:
|
|
465
|
+
name: sdist
|
|
466
|
+
path: dist
|
|
467
|
+
|
|
468
|
+
- name: Download wheels
|
|
469
|
+
uses: actions/download-artifact@v4
|
|
470
|
+
with:
|
|
471
|
+
pattern: wheels-*
|
|
472
|
+
path: dist
|
|
473
|
+
merge-multiple: true
|
|
474
|
+
|
|
475
|
+
- name: Publish to PyPI
|
|
476
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
477
|
+
with:
|
|
478
|
+
skip-existing: true
|
|
479
|
+
verbose: true
|
|
480
|
+
|
|
481
|
+
deploy-docs:
|
|
482
|
+
name: Deploy Documentation
|
|
483
|
+
needs: [prepare-release, publish-pypi]
|
|
484
|
+
if: needs.prepare-release.outputs.release_created == 'true'
|
|
485
|
+
uses: ./.github/workflows/publish-docs.yml
|
|
486
|
+
permissions:
|
|
487
|
+
contents: read
|
|
488
|
+
pages: write
|
|
489
|
+
id-token: write
|
|
490
|
+
with:
|
|
491
|
+
ref: ${{ needs.prepare-release.outputs.release_tag }}
|
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.3.0 (2026-04-22)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Align composite unique index names and harden Alembic/Rust handling
|
|
9
|
+
([`3350481`](https://github.com/syn54x/ferro-orm/commit/33504812d37d93bf69c2be8f6bee6f390803a460))
|
|
10
|
+
|
|
11
|
+
- Refresh Pydantic FieldInfo when reconciling shadow FK types
|
|
12
|
+
([`6cf1ac8`](https://github.com/syn54x/ferro-orm/commit/6cf1ac8c2e361df8de11795a8151c34d17a39445))
|
|
13
|
+
|
|
14
|
+
### Chores
|
|
15
|
+
|
|
16
|
+
- Remove doc
|
|
17
|
+
([`16e4028`](https://github.com/syn54x/ferro-orm/commit/16e4028f72fc47109b2511d1feb23811c831f32c))
|
|
18
|
+
|
|
19
|
+
### Continuous Integration
|
|
20
|
+
|
|
21
|
+
- Fix release
|
|
22
|
+
([`688d01b`](https://github.com/syn54x/ferro-orm/commit/688d01bdae0aff1f82e4a1bb60dd1b8ab35e1d01))
|
|
23
|
+
|
|
24
|
+
- Fix release
|
|
25
|
+
([`249e460`](https://github.com/syn54x/ferro-orm/commit/249e46058bac87215920083a9d45557f3c58b62f))
|
|
26
|
+
|
|
27
|
+
- Fix release
|
|
28
|
+
([`888e15e`](https://github.com/syn54x/ferro-orm/commit/888e15eff693d1e1bfa279d809e790e52cd7ce25))
|
|
29
|
+
|
|
30
|
+
- Fix release
|
|
31
|
+
([`58bb5b2`](https://github.com/syn54x/ferro-orm/commit/58bb5b2b0962481b3cfbf3ffbe6c6a2653b213c0))
|
|
32
|
+
|
|
33
|
+
- Reorder release steps to prevent tagging before checks are complete
|
|
34
|
+
([`ad1fd8d`](https://github.com/syn54x/ferro-orm/commit/ad1fd8d5ba08bdd7a1bcd257fff3fc12ff458c12))
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
|
|
38
|
+
- Complete documentation restructure and implementation summary
|
|
39
|
+
([`937e75e`](https://github.com/syn54x/ferro-orm/commit/937e75ee7b5c526aca776dd8409f9e0df5f0e892))
|
|
40
|
+
|
|
41
|
+
- Enhance shadow field documentation and clarify relationship resolution process
|
|
42
|
+
([`1d350fd`](https://github.com/syn54x/ferro-orm/commit/1d350fd728310a5b9a24f129986f873a84a8592f))
|
|
43
|
+
|
|
44
|
+
### Features
|
|
45
|
+
|
|
46
|
+
- Composite unique constraints and default M2M pair uniqueness
|
|
47
|
+
([`dc12880`](https://github.com/syn54x/ferro-orm/commit/dc12880b7b8676c088183edf1f32b48a36314448))
|
|
48
|
+
|
|
49
|
+
- Derive shadow FK types from related PK and reconcile after resolve
|
|
50
|
+
([`d3ae486`](https://github.com/syn54x/ferro-orm/commit/d3ae4862858ccd51f62d62a939e6a90b8efb8980))
|
|
51
|
+
|
|
52
|
+
### Testing
|
|
53
|
+
|
|
54
|
+
- UUID FK save reparenting and bulk_create coverage
|
|
55
|
+
([`6c93cea`](https://github.com/syn54x/ferro-orm/commit/6c93cea7906ac264b266342ecf71602c7aff6ed6))
|
|
56
|
+
|
|
57
|
+
|
|
4
58
|
## v0.2.1 (2026-04-20)
|
|
5
59
|
|
|
6
60
|
### Bug Fixes
|
|
@@ -68,7 +68,7 @@ graph TB
|
|
|
68
68
|
- Async runtime integration
|
|
69
69
|
|
|
70
70
|
**Data formats:**
|
|
71
|
-
- JSON schema (models → Rust)
|
|
71
|
+
- JSON schema (models → Rust), including Ferro-specific table-level keys such as `ferro_composite_uniques` alongside per-column metadata in `properties`
|
|
72
72
|
- Query AST (filters, joins → Rust)
|
|
73
73
|
- Binary rows (Rust → Python)
|
|
74
74
|
|
|
@@ -61,6 +61,10 @@ alembic revision --autogenerate -m "Initial schema"
|
|
|
61
61
|
|
|
62
62
|
Alembic compares your models to the database and generates a migration script in `migrations/versions/`.
|
|
63
63
|
|
|
64
|
+
### Composite uniques and Alembic
|
|
65
|
+
|
|
66
|
+
When you declare `__ferro_composite_uniques__` on a model, Ferro’s `get_metadata()` bridge adds matching SQLAlchemy `UniqueConstraint` objects to the reflected `Table`. Autogenerated revisions will therefore include those constraints (and the same for default many-to-many join tables, which get a composite unique on the two FK columns). Review generated migrations as usual before applying them in production.
|
|
67
|
+
|
|
64
68
|
### 4. Review the Migration
|
|
65
69
|
|
|
66
70
|
Open the generated file in `migrations/versions/xxxx_initial_schema.py`:
|
|
@@ -76,7 +76,7 @@ Both styles support the same database constraint parameters:
|
|
|
76
76
|
| :--- | :--- | :--- | :--- |
|
|
77
77
|
| `primary_key` | `bool` | `False` | Marks the field as the primary key for the table. |
|
|
78
78
|
| `autoincrement` | `bool \| None` | `None` | If `True`, the database generates values automatically. Defaults to `True` for integer primary keys. |
|
|
79
|
-
| `unique` | `bool` | `False` | Enforces a uniqueness constraint on
|
|
79
|
+
| `unique` | `bool` | `False` | Enforces a **single-column** uniqueness constraint on that column only. For uniqueness on a combination of columns, see [Composite unique constraints](#composite-unique-constraints) below. |
|
|
80
80
|
| `index` | `bool` | `False` | Creates a database index for this column to improve query performance. |
|
|
81
81
|
|
|
82
82
|
#### Examples
|
|
@@ -113,6 +113,40 @@ slug: str = Field(unique=True, index=True)
|
|
|
113
113
|
email: Annotated[str, FerroField(unique=True)]
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
### Composite unique constraints
|
|
117
|
+
|
|
118
|
+
Sometimes a row should be unique **across several columns together** (for example one membership per `(user_id, org_id)` pair). That is a *composite* unique: in SQL this is typically expressed as `UNIQUE (user_id, org_id)` on the table, or an equivalent unique index on those columns.
|
|
119
|
+
|
|
120
|
+
Ferro does **not** use `FerroField(unique=True)` for that case (`unique=True` is only per column). Instead, set the **`typing.ClassVar`** `__ferro_composite_uniques__` on your model (the base `Model` defines it as `()` so IDEs and type checkers know the hook exists; subclasses override when needed):
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from typing import Annotated, ClassVar
|
|
124
|
+
import uuid
|
|
125
|
+
|
|
126
|
+
from ferro import Model, FerroField
|
|
127
|
+
|
|
128
|
+
class OrgMembership(Model):
|
|
129
|
+
__ferro_composite_uniques__: ClassVar[tuple[tuple[str, ...], ...]] = (
|
|
130
|
+
("user_id", "org_id"),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
id: Annotated[uuid.UUID | None, FerroField(primary_key=True)] = None
|
|
134
|
+
user_id: Annotated[uuid.UUID, FerroField()]
|
|
135
|
+
org_id: Annotated[uuid.UUID, FerroField()]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- Each inner tuple lists **database column names** as they appear in the generated schema (the same names as your Pydantic fields for scalar columns; for `ForeignKey("user")` use `user_id` in the tuple).
|
|
139
|
+
- You can list several groups for multiple composite uniques on one model.
|
|
140
|
+
- Invalid or unknown column names raise when the model is registered.
|
|
141
|
+
|
|
142
|
+
**Null semantics (SQLite):** With the default local SQLite engine, `UNIQUE` treats `NULL` as distinct from other `NULL` values for multi-column constraints unless columns are `NOT NULL`. Ferro maps nullability from your types and defaults like other fields; optional composite columns can therefore allow multiple rows that differ only by `NULL` in a nullable column. Prefer `NOT NULL` on composite members when you need strict “at most one row per pair” semantics. Other databases can differ; consult your backend’s documentation when you target PostgreSQL, MySQL, and so on.
|
|
143
|
+
|
|
144
|
+
**Wire format:** Declarations use nested tuples in Python; the schema JSON sent to the Rust engine uses nested lists (`ferro_composite_uniques`) because JSON has no tuple type.
|
|
145
|
+
|
|
146
|
+
**Many-to-many join tables:** When you use `ManyToManyField` without a custom `through` table, Ferro creates a default join table with two foreign-key columns. That table automatically gets a composite unique on those two columns so the same link cannot be stored twice. If you already have duplicate rows in such a table, adding this constraint in a migration may require a data cleanup step first.
|
|
147
|
+
|
|
148
|
+
See also [Schema management / migrations](migrations.md) for how composite uniques appear in Alembic metadata.
|
|
149
|
+
|
|
116
150
|
**Indexes:**
|
|
117
151
|
|
|
118
152
|
```python
|
|
@@ -171,7 +205,7 @@ class Product(Model):
|
|
|
171
205
|
Ferro uses a custom `ModelMetaclass` to bridge Python and Rust:
|
|
172
206
|
|
|
173
207
|
1. **Schema Capture**: When you define a class, the metaclass inspects its fields and constraints.
|
|
174
|
-
2. **Rust Registration**: The schema is serialized to a JSON-AST and passed to the Rust core's `MODEL_REGISTRY`.
|
|
208
|
+
2. **Rust Registration**: The schema is serialized to a JSON-AST (including Ferro-specific keys such as `ferro_composite_uniques` when declared) and passed to the Rust core's `MODEL_REGISTRY`.
|
|
175
209
|
3. **Table Generation**: When `auto_migrate=True` is used or `create_tables()` is called, the Rust engine generates the appropriate SQL `CREATE TABLE` statements.
|
|
176
210
|
|
|
177
211
|
This architecture allows Ferro to leverage Rust's performance for SQL generation and row hydration while maintaining a pure Python interface.
|