ferro-orm 0.3.0__tar.gz → 0.3.1__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 → ferro_orm-0.3.1}/.github/PERMISSIONS.md +61 -32
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/workflows/release.yml +103 -42
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.gitignore +1 -2
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/CHANGELOG.md +32 -4
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/Cargo.lock +1 -1
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/Cargo.toml +1 -1
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/PKG-INFO +3 -4
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/README.md +2 -3
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/TEST_RESULTS.md +4 -2
- ferro_orm-0.3.1/docs/api/fields.md +21 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/coming-soon.md +6 -2
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/concepts/architecture.md +4 -2
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/concepts/performance.md +7 -3
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/getting-started/tutorial.md +13 -13
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/guide/migrations.md +35 -7
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/guide/models-and-fields.md +39 -26
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/guide/queries.md +3 -3
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/guide/relationships.md +1 -1
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/howto/pagination.md +5 -3
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/index.md +3 -3
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/migration-sqlalchemy.md +9 -6
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/pyproject.toml +5 -2
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/__init__.py +2 -1
- ferro_orm-0.3.1/src/ferro/_annotation_utils.py +28 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/base.py +50 -1
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/composite_uniques.py +3 -2
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/fields.py +18 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/metaclass.py +7 -1
- ferro_orm-0.3.1/src/ferro/migrations/alembic.py +345 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/models.py +18 -4
- ferro_orm-0.3.1/tests/test_alembic_autogenerate.py +190 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_alembic_bridge.py +21 -1
- ferro_orm-0.3.1/tests/test_alembic_nullability.py +270 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_alembic_type_mapping.py +3 -1
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_composite_unique.py +16 -16
- ferro_orm-0.3.1/tests/test_field_wrapper.py +95 -0
- ferro_orm-0.3.0/docs/api/fields.md +0 -21
- ferro_orm-0.3.0/src/ferro/migrations/alembic.py +0 -233
- ferro_orm-0.3.0/tests/test_alembic_autogenerate.py +0 -94
- ferro_orm-0.3.0/tests/test_field_wrapper.py +0 -47
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/.python-version +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/LICENSE +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/api/model.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/api/query.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/api/relationships.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/api/transactions.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/api/utilities.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/changelog.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/concepts/identity-map.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/concepts/type-safety.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/contributing.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/faq.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/getting-started/installation.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/getting-started/next-steps.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/guide/database.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/guide/mutations.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/guide/transactions.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/howto/multiple-databases.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/howto/soft-deletes.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/howto/testing.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/howto/timestamps.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/stylesheets/extra.css +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/docs/why-ferro.md +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/justfile +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/mkdocs.yml +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/connection.rs +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/py.typed +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/query/builder.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/ferro/state.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/lib.rs +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/operations.rs +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/query.rs +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/schema.rs +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/src/state.rs +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/conftest.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_connection.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_constraints.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_crud.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_deletion.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_helpers.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_hydration.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_metadata.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_models.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_refresh.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_schema.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_string_search.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/tests/test_transactions.py +0 -0
- {ferro_orm-0.3.0 → ferro_orm-0.3.1}/uv.lock +0 -0
|
@@ -45,7 +45,37 @@ All workflows use explicit, fine-grained permissions (principle of least privile
|
|
|
45
45
|
|
|
46
46
|
**Trigger:** Manual workflow dispatch
|
|
47
47
|
|
|
48
|
-
**
|
|
48
|
+
**Job graph (top to bottom is execution order):**
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
ci-gate packaging-smoke
|
|
52
|
+
\ /
|
|
53
|
+
prepare-release (computes bump, writes release.patch; no pushes)
|
|
54
|
+
/ | | \
|
|
55
|
+
build-wheels build-sdist test-wheels verify-docs
|
|
56
|
+
\ | /
|
|
57
|
+
promote-pypi (PyPI Trusted Publishing; first irreversible step)
|
|
58
|
+
|
|
|
59
|
+
promote-git (commit bump, push main, tag, create GH Release + assets)
|
|
60
|
+
|
|
|
61
|
+
promote-docs (deploys MkDocs site pinned to the new tag)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Nothing is pushed, tagged, or published until every validation job above
|
|
65
|
+
`promote-pypi` has succeeded. PyPI is published first so that if anything
|
|
66
|
+
fails, `main` is never advanced to an "orphan" version.
|
|
67
|
+
|
|
68
|
+
**Permissions (prepare-release):**
|
|
69
|
+
```yaml
|
|
70
|
+
permissions:
|
|
71
|
+
contents: read
|
|
72
|
+
issues: read
|
|
73
|
+
pull-requests: read
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Prepare-release only needs read access because it just computes the bump and uploads a patch artifact — it does not push anything.
|
|
77
|
+
|
|
78
|
+
**Permissions (promote-git):**
|
|
49
79
|
```yaml
|
|
50
80
|
permissions:
|
|
51
81
|
contents: write
|
|
@@ -53,32 +83,23 @@ permissions:
|
|
|
53
83
|
pull-requests: write
|
|
54
84
|
```
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
- Commit version bumps to pyproject.toml and Cargo.toml
|
|
86
|
+
- `contents: write` - Allows the job to:
|
|
87
|
+
- Commit version bumps to `pyproject.toml` and `Cargo.toml`
|
|
59
88
|
- Push commits to the `main` branch
|
|
60
|
-
- Create and push git tags (e.g., `v0.
|
|
61
|
-
- Create GitHub
|
|
89
|
+
- Create and push git tags (e.g., `v0.3.1`)
|
|
90
|
+
- Create GitHub Releases
|
|
62
91
|
|
|
63
|
-
- `issues: write` - Allows
|
|
64
|
-
|
|
65
|
-
- Close issues automatically via commit messages
|
|
66
|
-
- Add labels or comments to issues
|
|
92
|
+
- `issues: write` / `pull-requests: write` - Allows semantic-release to
|
|
93
|
+
update issue/PR references in generated release notes.
|
|
67
94
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
- Updates version in both Python and Rust files
|
|
77
|
-
- Finalizes CHANGELOG.md
|
|
78
|
-
- Creates git tag
|
|
79
|
-
- Creates GitHub release
|
|
80
|
-
- Triggers publish workflow
|
|
81
|
-
- Reports whether release commits include `CHANGELOG.md`
|
|
95
|
+
**What the release flow does:**
|
|
96
|
+
- Analyzes conventional commits since the latest tag (fetched explicitly via `fetch-tags: true`)
|
|
97
|
+
- Fails fast if the computed version collides with an existing tag (indicates a tag-fetch bug)
|
|
98
|
+
- Bumps version in `pyproject.toml` and `Cargo.toml` and finalizes `CHANGELOG.md`
|
|
99
|
+
- Validates the bumped tree across wheel builds, sdist, install smoke tests, and `mkdocs build`
|
|
100
|
+
- Publishes to PyPI before touching `main`
|
|
101
|
+
- Commits bump to `main`, creates the tag, creates the GitHub Release with generated notes, attaches wheels + sdist
|
|
102
|
+
- Deploys the MkDocs site pinned to the new tag
|
|
82
103
|
|
|
83
104
|
**Required Secrets:**
|
|
84
105
|
- `RELEASE_DEPLOY_KEY` (private SSH key for a write-enabled deploy key)
|
|
@@ -88,15 +109,15 @@ permissions:
|
|
|
88
109
|
|
|
89
110
|
### 2. Build & Publish (jobs in `release.yml`)
|
|
90
111
|
|
|
91
|
-
**Trigger:** Part of `release.yml`
|
|
112
|
+
**Trigger:** Part of `release.yml` — all build and publish jobs live in the same workflow file.
|
|
92
113
|
|
|
93
114
|
Build, test, and publish jobs are defined directly in `release.yml` so PyPI Trusted Publishing receives a token with `workflow_ref: release.yml` (reusable workflows are not supported by PyPI).
|
|
94
115
|
|
|
95
116
|
**Permissions:**
|
|
96
117
|
|
|
97
|
-
**For build-wheels, build-sdist, test-wheels:** (default - read-only)
|
|
118
|
+
**For build-wheels, build-sdist, test-wheels, verify-docs:** (default - read-only)
|
|
98
119
|
|
|
99
|
-
**For
|
|
120
|
+
**For promote-pypi job:**
|
|
100
121
|
```yaml
|
|
101
122
|
permissions:
|
|
102
123
|
id-token: write
|
|
@@ -106,10 +127,10 @@ permissions:
|
|
|
106
127
|
- `id-token: write` - Allows the job to request an OIDC token and authenticate with PyPI using Trusted Publishing.
|
|
107
128
|
|
|
108
129
|
**What It Does:**
|
|
109
|
-
- Builds wheels for multiple platforms
|
|
110
|
-
- Builds source distribution
|
|
111
|
-
-
|
|
112
|
-
-
|
|
130
|
+
- Builds wheels for multiple platforms against the patched (bumped) tree
|
|
131
|
+
- Builds source distribution against the patched tree
|
|
132
|
+
- Installs wheels and runs a smoke import + in-memory SQLite check
|
|
133
|
+
- `promote-pypi` publishes the validated wheels + sdist to PyPI via OIDC
|
|
113
134
|
|
|
114
135
|
---
|
|
115
136
|
|
|
@@ -268,10 +289,18 @@ Permission to manage GitHub Pages deployments:
|
|
|
268
289
|
**Cause:** Missing `id-token: write` permission
|
|
269
290
|
|
|
270
291
|
**Solution:**
|
|
271
|
-
- Ensure `
|
|
292
|
+
- Ensure `promote-pypi` job has `id-token: write`
|
|
272
293
|
- Verify PyPI trusted publisher is configured correctly
|
|
273
294
|
- Check environment name matches (`pypi`)
|
|
274
295
|
|
|
296
|
+
### Release says "0.3.0 has already been released!" for a version that was already shipped
|
|
297
|
+
|
|
298
|
+
**Cause:** `actions/checkout@v4` runs `git fetch --no-tags` by default, even with `fetch-depth: 0`. Without tags, `semantic-release` picks an older baseline tag, computes a minor/patch bump, and lands on the same version number as a prior release.
|
|
299
|
+
|
|
300
|
+
**Solution:**
|
|
301
|
+
- The `prepare-release` job sets `fetch-tags: true` on its checkout step and then runs a `git describe` vs `semantic-release --print-last-released-tag` comparison as a guard. Both must be present.
|
|
302
|
+
- If this check fails in a run, confirm no one has removed `fetch-tags: true` from the checkout step.
|
|
303
|
+
|
|
275
304
|
---
|
|
276
305
|
|
|
277
306
|
## Verification
|
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
|
|
25
25
|
# Bump versions and changelog locally only; export a patch so every later job
|
|
26
26
|
# validates the exact tree that would be released. Nothing is pushed until
|
|
27
|
-
#
|
|
27
|
+
# promote-* jobs run, after wheels, sdist, tests, and docs all pass.
|
|
28
28
|
prepare-release:
|
|
29
29
|
name: Prepare release (no push)
|
|
30
30
|
needs: [ci-gate, packaging-smoke]
|
|
@@ -48,6 +48,11 @@ jobs:
|
|
|
48
48
|
uses: actions/checkout@v4
|
|
49
49
|
with:
|
|
50
50
|
fetch-depth: 0
|
|
51
|
+
# actions/checkout@v4 passes --no-tags by default, even with
|
|
52
|
+
# fetch-depth: 0. semantic-release needs tags to pick the right
|
|
53
|
+
# baseline version; without them it may bump from an older tag and
|
|
54
|
+
# land on a version that has already been released.
|
|
55
|
+
fetch-tags: true
|
|
51
56
|
|
|
52
57
|
- name: Set up Python
|
|
53
58
|
uses: actions/setup-python@v5
|
|
@@ -63,6 +68,28 @@ jobs:
|
|
|
63
68
|
run: |
|
|
64
69
|
uv sync --only-group release --no-install-project --python 3.13
|
|
65
70
|
|
|
71
|
+
# Fail fast if the checkout did not materialize tags. Without this guard
|
|
72
|
+
# semantic-release silently picks an older baseline and can compute an
|
|
73
|
+
# already-released version (see PSR note: "No release will be made, X has
|
|
74
|
+
# already been released!").
|
|
75
|
+
- name: Verify tag visibility
|
|
76
|
+
env:
|
|
77
|
+
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
78
|
+
run: |
|
|
79
|
+
set -euo pipefail
|
|
80
|
+
GIT_LATEST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || echo '')"
|
|
81
|
+
PSR_LATEST_TAG="$(uv run semantic-release version --print-last-released-tag 2>/dev/null | tail -n1 || echo '')"
|
|
82
|
+
|
|
83
|
+
echo "git describe --tags --abbrev=0 -> '${GIT_LATEST_TAG}'"
|
|
84
|
+
echo "semantic-release --print-last-released-tag -> '${PSR_LATEST_TAG}'"
|
|
85
|
+
|
|
86
|
+
if [[ -n "${GIT_LATEST_TAG}" && "${GIT_LATEST_TAG}" != "${PSR_LATEST_TAG}" ]]; then
|
|
87
|
+
echo "ERROR: git and semantic-release disagree on the latest tag."
|
|
88
|
+
echo "This usually means actions/checkout did not fetch tags."
|
|
89
|
+
echo "Confirm 'fetch-tags: true' on the checkout step."
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
|
|
66
93
|
- name: Run semantic-release (local bump only)
|
|
67
94
|
env:
|
|
68
95
|
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
@@ -105,11 +132,21 @@ jobs:
|
|
|
105
132
|
fi
|
|
106
133
|
|
|
107
134
|
VERSION="$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")"
|
|
135
|
+
TAG="v${VERSION}"
|
|
136
|
+
|
|
137
|
+
# Final guard: if the tag already exists on the remote, bail before
|
|
138
|
+
# any irreversible promote-* step runs (PyPI, main push, GH Release).
|
|
139
|
+
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
|
|
140
|
+
echo "ERROR: tag ${TAG} already exists on the remote."
|
|
141
|
+
echo "This means semantic-release computed a version that is already released."
|
|
142
|
+
exit 1
|
|
143
|
+
fi
|
|
144
|
+
|
|
108
145
|
echo "release_created=true" >> "$GITHUB_OUTPUT"
|
|
109
146
|
echo "changelog_validated=passed" >> "$GITHUB_OUTPUT"
|
|
110
|
-
echo "release_tag
|
|
147
|
+
echo "release_tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
111
148
|
echo "release_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
112
|
-
echo "Prepared
|
|
149
|
+
echo "Prepared ${TAG} as patch against ${{ github.sha }}"
|
|
113
150
|
|
|
114
151
|
- name: Upload release patch
|
|
115
152
|
if: steps.verify_release.outputs.release_created == 'true'
|
|
@@ -359,11 +396,50 @@ jobs:
|
|
|
359
396
|
run: |
|
|
360
397
|
uv run --no-sync mkdocs build
|
|
361
398
|
|
|
362
|
-
|
|
363
|
-
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
# Promote phase: every validation above has passed. Irreversible steps now
|
|
401
|
+
# run in a strict order:
|
|
402
|
+
# promote-pypi -> publishes to PyPI (first irreversible action)
|
|
403
|
+
# promote-git -> commits bump, pushes main, creates tag, creates GH Release
|
|
404
|
+
# promote-docs -> deploys MkDocs site pinned to the new tag
|
|
405
|
+
# If promote-pypi fails, main is untouched and the run can be re-triggered.
|
|
406
|
+
# ---------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
promote-pypi:
|
|
409
|
+
name: Publish to PyPI
|
|
364
410
|
needs: [prepare-release, build-wheels, build-sdist, test-wheels, verify-docs]
|
|
365
411
|
if: needs.prepare-release.outputs.release_created == 'true'
|
|
366
412
|
runs-on: ubuntu-latest
|
|
413
|
+
environment:
|
|
414
|
+
name: pypi
|
|
415
|
+
url: https://pypi.org/p/ferro-orm
|
|
416
|
+
permissions:
|
|
417
|
+
id-token: write
|
|
418
|
+
steps:
|
|
419
|
+
- name: Download sdist
|
|
420
|
+
uses: actions/download-artifact@v4
|
|
421
|
+
with:
|
|
422
|
+
name: sdist
|
|
423
|
+
path: dist
|
|
424
|
+
|
|
425
|
+
- name: Download wheels
|
|
426
|
+
uses: actions/download-artifact@v4
|
|
427
|
+
with:
|
|
428
|
+
pattern: wheels-*
|
|
429
|
+
path: dist
|
|
430
|
+
merge-multiple: true
|
|
431
|
+
|
|
432
|
+
- name: Publish to PyPI
|
|
433
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
434
|
+
with:
|
|
435
|
+
skip-existing: true
|
|
436
|
+
verbose: true
|
|
437
|
+
|
|
438
|
+
promote-git:
|
|
439
|
+
name: Commit, tag, and create GitHub Release
|
|
440
|
+
needs: [prepare-release, promote-pypi]
|
|
441
|
+
if: needs.prepare-release.outputs.release_created == 'true'
|
|
442
|
+
runs-on: ubuntu-latest
|
|
367
443
|
permissions:
|
|
368
444
|
contents: write
|
|
369
445
|
issues: write
|
|
@@ -372,13 +448,15 @@ jobs:
|
|
|
372
448
|
- name: Disable Git CRLF conversion before checkout
|
|
373
449
|
shell: bash
|
|
374
450
|
run: git config --global core.autocrlf false
|
|
451
|
+
|
|
375
452
|
- name: Checkout repository
|
|
376
453
|
uses: actions/checkout@v4
|
|
377
454
|
with:
|
|
378
455
|
ref: ${{ needs.prepare-release.outputs.release_base_sha }}
|
|
379
456
|
fetch-depth: 0
|
|
457
|
+
fetch-tags: true
|
|
380
458
|
ssh-key: ${{ secrets.RELEASE_DEPLOY_KEY }}
|
|
381
|
-
# Required so the deploy key stays loaded for git push
|
|
459
|
+
# Required so the deploy key stays loaded for git push below.
|
|
382
460
|
persist-credentials: true
|
|
383
461
|
|
|
384
462
|
- name: Download release patch
|
|
@@ -401,8 +479,10 @@ jobs:
|
|
|
401
479
|
git config user.name "github-actions[bot]"
|
|
402
480
|
git remote set-url origin "git@github.com:${{ github.repository }}.git"
|
|
403
481
|
|
|
482
|
+
# Re-check remote (prepare-release already checked, but the window
|
|
483
|
+
# between jobs is non-zero).
|
|
404
484
|
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
|
|
405
|
-
echo "ERROR: tag ${TAG}
|
|
485
|
+
echo "ERROR: tag ${TAG} appeared on the remote between jobs."
|
|
406
486
|
exit 1
|
|
407
487
|
fi
|
|
408
488
|
|
|
@@ -412,7 +492,7 @@ jobs:
|
|
|
412
492
|
-m "Automatically generated by python-semantic-release"
|
|
413
493
|
git push origin "HEAD:refs/heads/${{ github.ref_name }}"
|
|
414
494
|
|
|
415
|
-
git tag "${TAG}"
|
|
495
|
+
git tag -a "${TAG}" -m "Release ${TAG}"
|
|
416
496
|
git push origin "refs/tags/${TAG}"
|
|
417
497
|
|
|
418
498
|
# semantic-release matches branch config (e.g. branches.main); detached HEAD fails with
|
|
@@ -430,57 +510,38 @@ jobs:
|
|
|
430
510
|
with:
|
|
431
511
|
enable-cache: true
|
|
432
512
|
|
|
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
513
|
- name: Install dependencies
|
|
442
514
|
run: |
|
|
443
515
|
uv sync --only-group release --no-install-project --python 3.13
|
|
444
516
|
|
|
445
|
-
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
517
|
+
# semantic-release publish only uploads assets to an existing GitHub
|
|
518
|
+
# Release. Create the release with generated notes first, then attach
|
|
519
|
+
# wheels + sdist from the validation jobs.
|
|
520
|
+
- name: Download sdist for GitHub Release assets
|
|
463
521
|
uses: actions/download-artifact@v4
|
|
464
522
|
with:
|
|
465
523
|
name: sdist
|
|
466
524
|
path: dist
|
|
467
525
|
|
|
468
|
-
- name: Download wheels
|
|
526
|
+
- name: Download wheels for GitHub Release assets
|
|
469
527
|
uses: actions/download-artifact@v4
|
|
470
528
|
with:
|
|
471
529
|
pattern: wheels-*
|
|
472
530
|
path: dist
|
|
473
531
|
merge-multiple: true
|
|
474
532
|
|
|
475
|
-
- name:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
533
|
+
- name: Create GitHub Release and upload assets
|
|
534
|
+
env:
|
|
535
|
+
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
536
|
+
run: |
|
|
537
|
+
set -euo pipefail
|
|
538
|
+
TAG="${{ needs.prepare-release.outputs.release_tag }}"
|
|
539
|
+
uv run semantic-release changelog --post-to-release-tag "$TAG"
|
|
540
|
+
uv run semantic-release publish --tag "$TAG"
|
|
480
541
|
|
|
481
|
-
|
|
542
|
+
promote-docs:
|
|
482
543
|
name: Deploy Documentation
|
|
483
|
-
needs: [prepare-release,
|
|
544
|
+
needs: [prepare-release, promote-git]
|
|
484
545
|
if: needs.prepare-release.outputs.release_created == 'true'
|
|
485
546
|
uses: ./.github/workflows/publish-docs.yml
|
|
486
547
|
permissions:
|
|
@@ -1,7 +1,38 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
## v0.3.
|
|
4
|
+
## v0.3.1 (2026-04-23)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Alembic autogenerate named SQLAlchemy enums for PostgreSQL
|
|
9
|
+
([`25a00e8`](https://github.com/syn54x/ferro-orm/commit/25a00e84502ae1f8ba502718934d93eedfa4ce09))
|
|
10
|
+
|
|
11
|
+
- **migrations**: Align nullable inference with field types
|
|
12
|
+
([`885f0fe`](https://github.com/syn54x/ferro-orm/commit/885f0fe155dfa643e29b9425ff1ede62f3f0b269))
|
|
13
|
+
|
|
14
|
+
- **migrations**: Propagate ForeignKey(unique=True) to Alembic metadata
|
|
15
|
+
([#22](https://github.com/syn54x/ferro-orm/pull/22),
|
|
16
|
+
[`9329e8f`](https://github.com/syn54x/ferro-orm/commit/9329e8fba2f0efd201bea4545393654c7d1dd34e))
|
|
17
|
+
|
|
18
|
+
### Continuous Integration
|
|
19
|
+
|
|
20
|
+
- Fix release
|
|
21
|
+
([`e2822f6`](https://github.com/syn54x/ferro-orm/commit/e2822f6c9bacc6fc955e56b2ca8e120cc22b0b72))
|
|
22
|
+
|
|
23
|
+
- Fix release
|
|
24
|
+
([`e5c1adc`](https://github.com/syn54x/ferro-orm/commit/e5c1adcc10eb44845ef95d78226840ecdbfd0ebd))
|
|
25
|
+
|
|
26
|
+
- Fix release
|
|
27
|
+
([`688d01b`](https://github.com/syn54x/ferro-orm/commit/688d01bdae0aff1f82e4a1bb60dd1b8ab35e1d01))
|
|
28
|
+
|
|
29
|
+
### Documentation
|
|
30
|
+
|
|
31
|
+
- Prefer Field over FerroField
|
|
32
|
+
([`3385cfa`](https://github.com/syn54x/ferro-orm/commit/3385cfadf0951f80827dac1aa08f73430a02023f))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## v0.3.0 (2026-04-23)
|
|
5
36
|
|
|
6
37
|
### Bug Fixes
|
|
7
38
|
|
|
@@ -18,9 +49,6 @@
|
|
|
18
49
|
|
|
19
50
|
### Continuous Integration
|
|
20
51
|
|
|
21
|
-
- Fix release
|
|
22
|
-
([`688d01b`](https://github.com/syn54x/ferro-orm/commit/688d01bdae0aff1f82e4a1bb60dd1b8ab35e1d01))
|
|
23
|
-
|
|
24
52
|
- Fix release
|
|
25
53
|
([`249e460`](https://github.com/syn54x/ferro-orm/commit/249e46058bac87215920083a9d45557f3c58b62f))
|
|
26
54
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ferro-orm
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Requires-Dist: pydantic>=2.0
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Summary: A high-performance, Rust-backed ORM for Python.
|
|
@@ -55,11 +55,10 @@ pip install "ferro-orm[alembic]"
|
|
|
55
55
|
|
|
56
56
|
```python
|
|
57
57
|
import asyncio
|
|
58
|
-
from
|
|
59
|
-
from ferro import Model, FerroField, connect
|
|
58
|
+
from ferro import Field, Model, connect
|
|
60
59
|
|
|
61
60
|
class User(Model):
|
|
62
|
-
id:
|
|
61
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
63
62
|
username: str
|
|
64
63
|
is_active: bool = True
|
|
65
64
|
|
|
@@ -45,11 +45,10 @@ pip install "ferro-orm[alembic]"
|
|
|
45
45
|
|
|
46
46
|
```python
|
|
47
47
|
import asyncio
|
|
48
|
-
from
|
|
49
|
-
from ferro import Model, FerroField, connect
|
|
48
|
+
from ferro import Field, Model, connect
|
|
50
49
|
|
|
51
50
|
class User(Model):
|
|
52
|
-
id:
|
|
51
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
53
52
|
username: str
|
|
54
53
|
is_active: bool = True
|
|
55
54
|
|
|
@@ -27,7 +27,7 @@ All field types and constraints work as documented:
|
|
|
27
27
|
- Basic model definition ✅
|
|
28
28
|
- All documented field types (str, int, Decimal, date, dict, Enum) ✅
|
|
29
29
|
- Enum field type with proper serialization/deserialization ✅
|
|
30
|
-
- Field() Pydantic-style constraints ✅
|
|
30
|
+
- Field() Pydantic-style constraints ✅ (preferred in user-facing docs)
|
|
31
31
|
- FerroField() Annotated-style constraints ✅
|
|
32
32
|
- Unique constraints ✅
|
|
33
33
|
|
|
@@ -131,8 +131,10 @@ await User.where(User.role.in_([UserRole.ADMIN.value, UserRole.MODERATOR.value])
|
|
|
131
131
|
**Working Pattern**: Primary keys should be optional with None default:
|
|
132
132
|
|
|
133
133
|
```python
|
|
134
|
+
from ferro import Field, Model
|
|
135
|
+
|
|
134
136
|
class User(Model):
|
|
135
|
-
id:
|
|
137
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
136
138
|
username: str
|
|
137
139
|
```
|
|
138
140
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Fields API
|
|
2
|
+
|
|
3
|
+
Complete reference for field types and metadata. In application models, use **`ferro.Field`** in either the **assignment** or **annotation** pattern described in the [models & fields guide](../guide/models-and-fields.md#field-constraints). **`FerroField`** is the lower-level metadata type; `Field(...)` is normalized into the same shape internally.
|
|
4
|
+
|
|
5
|
+
## Field
|
|
6
|
+
|
|
7
|
+
::: ferro.fields.Field
|
|
8
|
+
options:
|
|
9
|
+
show_source: false
|
|
10
|
+
heading_level: 3
|
|
11
|
+
|
|
12
|
+
## FerroField
|
|
13
|
+
|
|
14
|
+
::: ferro.base.FerroField
|
|
15
|
+
options:
|
|
16
|
+
show_source: false
|
|
17
|
+
heading_level: 3
|
|
18
|
+
|
|
19
|
+
## See Also
|
|
20
|
+
|
|
21
|
+
- [Models & Fields Guide](../guide/models-and-fields.md)
|
|
@@ -429,12 +429,16 @@ Many-to-many relationships are defined with `ManyToManyField`, but the join tabl
|
|
|
429
429
|
|
|
430
430
|
**Example (Partially Working):**
|
|
431
431
|
```python
|
|
432
|
+
from typing import Annotated
|
|
433
|
+
|
|
434
|
+
from ferro import BackRef, Field, ManyToManyField, Model
|
|
435
|
+
|
|
432
436
|
class Post(Model):
|
|
433
|
-
id:
|
|
437
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
434
438
|
tags: Annotated[list["Tag"], ManyToManyField(related_name="posts")] = None
|
|
435
439
|
|
|
436
440
|
class Tag(Model):
|
|
437
|
-
id:
|
|
441
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
438
442
|
posts: BackRef[list["Post"]] | None = None
|
|
439
443
|
|
|
440
444
|
# Models created, but join table 'post_tags' is NOT auto-created
|
|
@@ -192,9 +192,11 @@ sequenceDiagram
|
|
|
192
192
|
|
|
193
193
|
Python model:
|
|
194
194
|
```python
|
|
195
|
+
from ferro import Field, Model
|
|
196
|
+
|
|
195
197
|
class User(Model):
|
|
196
|
-
id:
|
|
197
|
-
username:
|
|
198
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
199
|
+
username: str = Field(unique=True)
|
|
198
200
|
email: str
|
|
199
201
|
```
|
|
200
202
|
|
|
@@ -102,10 +102,14 @@ await User.where(User.is_active == False).update(status="archived")
|
|
|
102
102
|
### 3. Index Frequently Filtered Fields
|
|
103
103
|
|
|
104
104
|
```python
|
|
105
|
+
from datetime import datetime
|
|
106
|
+
|
|
107
|
+
from ferro import Field, Model
|
|
108
|
+
|
|
105
109
|
class User(Model):
|
|
106
|
-
email:
|
|
107
|
-
status:
|
|
108
|
-
created_at:
|
|
110
|
+
email: str = Field(unique=True, index=True)
|
|
111
|
+
status: str = Field(index=True)
|
|
112
|
+
created_at: datetime = Field(index=True)
|
|
109
113
|
```
|
|
110
114
|
|
|
111
115
|
### 4. Use `.exists()` Instead of `.count()`
|
|
@@ -26,17 +26,17 @@ Let's create a blog with users, posts, and comments:
|
|
|
26
26
|
import asyncio
|
|
27
27
|
from datetime import datetime
|
|
28
28
|
from typing import Annotated
|
|
29
|
-
from ferro import Model,
|
|
29
|
+
from ferro import Model, Field, ForeignKey, BackRef, connect
|
|
30
30
|
|
|
31
31
|
class User(Model):
|
|
32
|
-
id:
|
|
33
|
-
username:
|
|
34
|
-
email:
|
|
32
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
33
|
+
username: str = Field(unique=True)
|
|
34
|
+
email: str = Field(unique=True)
|
|
35
35
|
posts: BackRef[list["Post"]] | None = None
|
|
36
36
|
comments: BackRef[list["Comment"]] | None = None
|
|
37
37
|
|
|
38
38
|
class Post(Model):
|
|
39
|
-
id:
|
|
39
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
40
40
|
title: str
|
|
41
41
|
content: str
|
|
42
42
|
published: bool = False
|
|
@@ -45,7 +45,7 @@ class Post(Model):
|
|
|
45
45
|
comments: BackRef[list["Comment"]] | None = None
|
|
46
46
|
|
|
47
47
|
class Comment(Model):
|
|
48
|
-
id:
|
|
48
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
49
49
|
text: str
|
|
50
50
|
created_at: datetime = datetime.now()
|
|
51
51
|
author: Annotated[User, ForeignKey(related_name="comments")]
|
|
@@ -305,17 +305,17 @@ Here's the full tutorial code:
|
|
|
305
305
|
import asyncio
|
|
306
306
|
from datetime import datetime
|
|
307
307
|
from typing import Annotated
|
|
308
|
-
from ferro import Model,
|
|
308
|
+
from ferro import Model, Field, ForeignKey, BackRef, connect
|
|
309
309
|
|
|
310
310
|
class User(Model):
|
|
311
|
-
id:
|
|
312
|
-
username:
|
|
313
|
-
email:
|
|
311
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
312
|
+
username: str = Field(unique=True)
|
|
313
|
+
email: str = Field(unique=True)
|
|
314
314
|
posts: BackRef[list["Post"]] | None = None
|
|
315
315
|
comments: BackRef[list["Comment"]] | None = None
|
|
316
316
|
|
|
317
317
|
class Post(Model):
|
|
318
|
-
id:
|
|
318
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
319
319
|
title: str
|
|
320
320
|
content: str
|
|
321
321
|
published: bool = False
|
|
@@ -324,7 +324,7 @@ class Post(Model):
|
|
|
324
324
|
comments: BackRef[list["Comment"]] | None = None
|
|
325
325
|
|
|
326
326
|
class Comment(Model):
|
|
327
|
-
id:
|
|
327
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
328
328
|
text: str
|
|
329
329
|
created_at: datetime = datetime.now()
|
|
330
330
|
author: Annotated[User, ForeignKey(related_name="comments")]
|
|
@@ -367,7 +367,7 @@ if __name__ == "__main__":
|
|
|
367
367
|
In this tutorial, you learned:
|
|
368
368
|
|
|
369
369
|
✅ How to define models with `Model` and type hints
|
|
370
|
-
✅ How to add constraints with `
|
|
370
|
+
✅ How to add constraints with `Field()` (assignment or `Annotated[..., Field(...)]`)
|
|
371
371
|
✅ How to create relationships with `ForeignKey` and `BackRef`
|
|
372
372
|
✅ How to connect to a database with `connect()`
|
|
373
373
|
✅ How to create records with `.create()`
|