openodd-py 0.2.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.
Files changed (46) hide show
  1. openodd_py-0.2.1/.github/workflows/ci.yml +35 -0
  2. openodd_py-0.2.1/.github/workflows/release-label.yml +72 -0
  3. openodd_py-0.2.1/.github/workflows/release.yml +322 -0
  4. openodd_py-0.2.1/.gitignore +7 -0
  5. openodd_py-0.2.1/Cargo.lock +731 -0
  6. openodd_py-0.2.1/Cargo.toml +36 -0
  7. openodd_py-0.2.1/LICENSE +202 -0
  8. openodd_py-0.2.1/NOTICE +7 -0
  9. openodd_py-0.2.1/PKG-INFO +70 -0
  10. openodd_py-0.2.1/README.md +54 -0
  11. openodd_py-0.2.1/docs/versioning.md +98 -0
  12. openodd_py-0.2.1/pyproject.toml +31 -0
  13. openodd_py-0.2.1/python/openodd/__init__.py +122 -0
  14. openodd_py-0.2.1/python/openodd/_core.pyi +56 -0
  15. openodd_py-0.2.1/python/openodd/backend.py +160 -0
  16. openodd_py-0.2.1/python/openodd/items.py +270 -0
  17. openodd_py-0.2.1/python/openodd/loader.py +193 -0
  18. openodd_py-0.2.1/python/openodd/model.py +585 -0
  19. openodd_py-0.2.1/python/openodd/odds/default/default.binding.yaml +78 -0
  20. openodd_py-0.2.1/python/openodd/odds/default/default.yaml +23 -0
  21. openodd_py-0.2.1/python/openodd/py.typed +0 -0
  22. openodd_py-0.2.1/python/openodd/report.py +190 -0
  23. openodd_py-0.2.1/python/openodd/sampler.py +299 -0
  24. openodd_py-0.2.1/python/openodd/sources.py +37 -0
  25. openodd_py-0.2.1/src/error.rs +40 -0
  26. openodd_py-0.2.1/src/items.rs +347 -0
  27. openodd_py-0.2.1/src/lib.rs +37 -0
  28. openodd_py-0.2.1/src/loader.rs +2321 -0
  29. openodd_py-0.2.1/src/model.rs +1248 -0
  30. openodd_py-0.2.1/src/python.rs +1574 -0
  31. openodd_py-0.2.1/src/report.rs +952 -0
  32. openodd_py-0.2.1/src/sampler.rs +873 -0
  33. openodd_py-0.2.1/src/sources.rs +268 -0
  34. openodd_py-0.2.1/src/transmission.rs +327 -0
  35. openodd_py-0.2.1/src/units.rs +247 -0
  36. openodd_py-0.2.1/src/value.rs +232 -0
  37. openodd_py-0.2.1/src/yaml.rs +220 -0
  38. openodd_py-0.2.1/tests/differential/cases.py +328 -0
  39. openodd_py-0.2.1/tests/differential/compare.py +35 -0
  40. openodd_py-0.2.1/tests/differential/difftest_probes.py +16 -0
  41. openodd_py-0.2.1/tests/differential/report_run.py +74 -0
  42. openodd_py-0.2.1/tests/differential/run.py +83 -0
  43. openodd_py-0.2.1/tests/test_api.py +233 -0
  44. openodd_py-0.2.1/tests/test_differential.py +47 -0
  45. openodd_py-0.2.1/tests/test_sampling.py +115 -0
  46. openodd_py-0.2.1/tools/bump_version.py +100 -0
@@ -0,0 +1,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ rust:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: dtolnay/rust-toolchain@stable
14
+ with:
15
+ components: clippy, rustfmt
16
+ - uses: Swatinem/rust-cache@v2
17
+ - run: cargo fmt --check
18
+ - run: cargo clippy --all-features --all-targets -- -D warnings
19
+ - run: cargo test
20
+
21
+ python:
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ matrix:
25
+ python: ["3.10", "3.12", "3.13"]
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: dtolnay/rust-toolchain@stable
29
+ - uses: Swatinem/rust-cache@v2
30
+ - uses: astral-sh/setup-uv@v6
31
+ with:
32
+ python-version: ${{ matrix.python }}
33
+ - run: uv venv && uv pip install maturin pytest hypothesis pyyaml
34
+ - run: uv run --no-project maturin develop
35
+ - run: uv run --no-project pytest -q
@@ -0,0 +1,72 @@
1
+ name: Release label
2
+
3
+ # The release workflow reads `release:major|minor|patch` off a PR once it is merged
4
+ # into main. This checks the same thing while the PR is still open, so a mistake shows
5
+ # up as a red check rather than as a failed (or missing) release after the merge:
6
+ #
7
+ # - the three labels exist in the repository, and are created if they do not;
8
+ # - the PR carries at most one of them (the release job refuses more than one);
9
+ # - the job summary says which bump the merge will cut, or that it cuts none.
10
+ #
11
+ # Its own workflow rather than a job in ci.yml, so that adding or removing a label
12
+ # re-runs this check and not the whole build.
13
+
14
+ on:
15
+ pull_request:
16
+ types: [opened, reopened, synchronize, labeled, unlabeled]
17
+
18
+ permissions:
19
+ contents: read
20
+ issues: write # create a missing release label
21
+ pull-requests: read
22
+
23
+ jobs:
24
+ release-label:
25
+ name: release label
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - name: the release labels exist in the repository
29
+ env:
30
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31
+ REPO: ${{ github.repository }}
32
+ run: |
33
+ set -euo pipefail
34
+ existing=$(gh api "repos/$REPO/labels" --paginate --jq '.[].name')
35
+ for spec in "major:d73a4a:Merging this PR cuts a major release" \
36
+ "minor:fbca04:Merging this PR cuts a minor release" \
37
+ "patch:0e8a16:Merging this PR cuts a patch release"; do
38
+ IFS=: read -r lvl color desc <<< "$spec"
39
+ name="release:$lvl"
40
+ if grep -qxF "$name" <<< "$existing"; then
41
+ echo "$name exists"
42
+ continue
43
+ fi
44
+ # A PR from a fork gets a read-only token, so creating can fail; say what
45
+ # is missing instead of an opaque API error.
46
+ if gh api "repos/$REPO/labels" -f name="$name" -f color="$color" \
47
+ -f description="$desc" > /dev/null; then
48
+ echo "created $name"
49
+ else
50
+ echo "::error::label $name does not exist and could not be created"
51
+ exit 1
52
+ fi
53
+ done
54
+
55
+ - name: at most one release label on this PR
56
+ env:
57
+ LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
58
+ run: |
59
+ set -euo pipefail
60
+ found=$(jq -r '.[] | select(test("^release:(major|minor|patch)$"))' <<< "$LABELS")
61
+ count=$(grep -c . <<< "$found" || true)
62
+ if [ "$count" -gt 1 ]; then
63
+ echo "::error::More than one release label on the PR ($(paste -sd, <<< "$found")) -- pick exactly one"
64
+ exit 1
65
+ fi
66
+ if [ "$count" -eq 1 ]; then
67
+ msg="Merging this PR cuts a **${found#release:}** release."
68
+ else
69
+ msg="No release label: merging this PR cuts no release."
70
+ fi
71
+ echo "$msg"
72
+ echo "$msg" >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,322 @@
1
+ # Cuts releases and publishes them to the GitHub release and PyPI. The mechanism is simple_lanelet2's.
2
+ #
3
+ # There are three ways in, and the first is the everyday one:
4
+ #
5
+ # push to main when a merged PR carries exactly one `release:major|minor|patch`
6
+ # label, the `release` job bumps the version, tags it `vX.Y.Z`, and the
7
+ # tag is built and published -- all in this one run.
8
+ # push a v* tag a hand-cut tag (a hotfix, say) is built and published directly; the
9
+ # `release` job is skipped.
10
+ # workflow_dispatch builds the wheels and sdist without tagging or publishing, to
11
+ # eyeball a release before committing to it.
12
+ #
13
+ # The publish happens as a follow-up job here rather than in a separate `on: push:
14
+ # tags` workflow on purpose: the auto-release tag is pushed with GITHUB_TOKEN, and a
15
+ # token-pushed tag does not start new workflow runs.
16
+ #
17
+ # Every release's wheels and sdist are attached to its GitHub release, whose assets
18
+ # page is a find-links index (`uv`/`pip --find-links
19
+ # https://github.com/hakuturu583/openodd_py/releases/expanded_assets/<tag>`).
20
+ #
21
+ # Every release is also published to PyPI with Trusted Publishing, so no token is
22
+ # stored here. Configure it once on PyPI: project openodd-py, owner hakuturu583,
23
+ # repository openodd_py, workflow release.yml, environment pypi. Until that exists the
24
+ # publish job fails, while the GitHub release still carries the files.
25
+ #
26
+ # A version can never be replaced on PyPI, so the tests run again here rather than
27
+ # trusting that CI was green on the same commit.
28
+
29
+ name: Release
30
+
31
+ on:
32
+ push:
33
+ branches: [main]
34
+ tags: ["v*"]
35
+ workflow_dispatch:
36
+
37
+ permissions:
38
+ contents: read
39
+
40
+ jobs:
41
+ # Read the release label off the merged PR and, if there is one, bump the version
42
+ # and push the tag. Runs only on a push to main; a tag push or a manual dispatch
43
+ # skips it and falls straight through to the build.
44
+ release:
45
+ name: cut the release tag
46
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
47
+ runs-on: ubuntu-latest
48
+ permissions:
49
+ contents: write
50
+ pull-requests: read # the commit -> PR lookup below
51
+ # Two release merges close together would both bump the same main and race on the
52
+ # push. Queue them instead: the second checks out main only once the first has
53
+ # pushed its bump, so it bumps from there.
54
+ concurrency:
55
+ group: release-bump
56
+ cancel-in-progress: false
57
+ outputs:
58
+ bump: ${{ steps.pr.outputs.bump }}
59
+ tag: ${{ steps.bump.outputs.tag }}
60
+ steps:
61
+ - name: read the release label off the merged PR
62
+ id: pr
63
+ env:
64
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65
+ run: |
66
+ set -euo pipefail
67
+ # A failed lookup fails the job: swallowing it would read as "no PR" and
68
+ # drop a labelled merge's release without a trace.
69
+ pr_json=$(gh api repos/${{ github.repository }}/commits/${{ github.sha }}/pulls \
70
+ --jq '.[0] // empty')
71
+ if [ -z "$pr_json" ]; then
72
+ echo "No PR associated with this push -- nothing to release"
73
+ echo "bump=none" >> "$GITHUB_OUTPUT"
74
+ exit 0
75
+ fi
76
+
77
+ labels=$(echo "$pr_json" | jq -r '[.labels[].name] | join(",")')
78
+ bump=none
79
+ count=0
80
+ for lvl in major minor patch; do
81
+ if echo ",$labels," | grep -q ",release:${lvl},"; then
82
+ bump=$lvl
83
+ count=$((count + 1))
84
+ fi
85
+ done
86
+ if [ "$count" -gt 1 ]; then
87
+ echo "::error::More than one release label on the PR -- pick exactly one"
88
+ exit 1
89
+ fi
90
+ echo "bump=${bump}" >> "$GITHUB_OUTPUT"
91
+ echo "Resolved bump level: ${bump}"
92
+
93
+ - uses: actions/checkout@v4
94
+ if: steps.pr.outputs.bump != 'none'
95
+ with:
96
+ ref: main
97
+ fetch-depth: 0
98
+
99
+ - name: bump Cargo.toml and Cargo.lock together
100
+ if: steps.pr.outputs.bump != 'none'
101
+ id: bump
102
+ run: |
103
+ set -euo pipefail
104
+ next=$(python3 tools/bump_version.py "${{ steps.pr.outputs.bump }}")
105
+ echo "next=${next}" >> "$GITHUB_OUTPUT"
106
+ echo "tag=v${next}" >> "$GITHUB_OUTPUT"
107
+ echo "Bumped to ${next} (${{ steps.pr.outputs.bump }})"
108
+
109
+ - name: fail if the tag already exists
110
+ if: steps.pr.outputs.bump != 'none'
111
+ run: |
112
+ if git rev-parse "refs/tags/${{ steps.bump.outputs.tag }}" >/dev/null 2>&1; then
113
+ echo "Tag ${{ steps.bump.outputs.tag }} already exists" >&2
114
+ exit 1
115
+ fi
116
+
117
+ - name: commit, tag and push
118
+ if: steps.pr.outputs.bump != 'none'
119
+ run: |
120
+ set -euo pipefail
121
+ git config user.name "github-actions[bot]"
122
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
123
+ git add Cargo.toml Cargo.lock
124
+ git commit -m "chore(release): ${{ steps.bump.outputs.tag }}"
125
+ git tag -a "${{ steps.bump.outputs.tag }}" -m "Release ${{ steps.bump.outputs.tag }}"
126
+ git push origin "HEAD:main"
127
+ git push origin "${{ steps.bump.outputs.tag }}"
128
+
129
+ # One place to work out, for all three entry points, which commit to build and whether
130
+ # it may be published. Runs even when `release` is skipped (tag push, dispatch).
131
+ resolve:
132
+ name: resolve what to build
133
+ needs: [release]
134
+ if: always()
135
+ runs-on: ubuntu-latest
136
+ outputs:
137
+ proceed: ${{ steps.r.outputs.proceed }}
138
+ ref: ${{ steps.r.outputs.ref }}
139
+ publish: ${{ steps.r.outputs.publish }}
140
+ version: ${{ steps.r.outputs.version }}
141
+ steps:
142
+ - id: r
143
+ env:
144
+ EVENT: ${{ github.event_name }}
145
+ REF: ${{ github.ref }}
146
+ REF_NAME: ${{ github.ref_name }}
147
+ BUMP: ${{ needs.release.outputs.bump }}
148
+ TAG: ${{ needs.release.outputs.tag }}
149
+ run: |
150
+ set -euo pipefail
151
+ proceed=false; publish=false; ref="$REF_NAME"
152
+ if [ "$EVENT" = "push" ] && [[ "$REF" == refs/tags/v* ]]; then
153
+ # A hand-cut tag: build and publish it.
154
+ proceed=true; publish=true; ref="$REF_NAME"
155
+ elif [ "$EVENT" = "push" ] && [ "$REF" = "refs/heads/main" ]; then
156
+ # The auto-release path: build and publish only if a tag was cut.
157
+ if [ "$BUMP" != "none" ] && [ -n "$TAG" ]; then
158
+ proceed=true; publish=true; ref="$TAG"
159
+ fi
160
+ elif [ "$EVENT" = "workflow_dispatch" ]; then
161
+ # A dry run: build the artifacts, publish nothing.
162
+ proceed=true; publish=false; ref="$REF_NAME"
163
+ fi
164
+ version=""
165
+ if [ "$publish" = true ]; then version="${ref#v}"; fi
166
+ echo "proceed=$proceed" >> "$GITHUB_OUTPUT"
167
+ echo "publish=$publish" >> "$GITHUB_OUTPUT"
168
+ echo "ref=$ref" >> "$GITHUB_OUTPUT"
169
+ echo "version=$version" >> "$GITHUB_OUTPUT"
170
+ echo "event=$EVENT ref=$ref proceed=$proceed publish=$publish version=$version"
171
+
172
+ verify:
173
+ name: test the tagged commit
174
+ needs: [resolve]
175
+ # `release` is skipped on a tag push or a dispatch, and a skipped ancestor skips
176
+ # every job downstream unless its condition says otherwise -- hence `!cancelled()`.
177
+ if: ${{ !cancelled() && needs.resolve.result == 'success' && needs.resolve.outputs.proceed == 'true' }}
178
+ runs-on: ubuntu-latest
179
+ steps:
180
+ - uses: actions/checkout@v4
181
+ with:
182
+ ref: ${{ needs.resolve.outputs.ref }}
183
+ - uses: dtolnay/rust-toolchain@stable
184
+ - uses: astral-sh/setup-uv@v6
185
+ with:
186
+ python-version: "3.12"
187
+
188
+ # pyproject.toml declares the version dynamic, so Cargo.toml is the one source;
189
+ # the lock file must agree with it or `cargo --locked` fails downstream.
190
+ - name: the version in the tag must be the version in the package
191
+ if: needs.resolve.outputs.publish == 'true'
192
+ env:
193
+ TAG: ${{ needs.resolve.outputs.version }}
194
+ run: |
195
+ CRATE=$(python3 tools/bump_version.py --current)
196
+ echo "tag=$TAG cargo=$CRATE"
197
+ test "$TAG" = "$CRATE"
198
+ cargo metadata --locked --format-version 1 --no-deps > /dev/null
199
+
200
+ - run: cargo test --locked
201
+ - run: uv venv && uv pip install maturin pytest hypothesis pyyaml
202
+ - run: uv run --no-project maturin develop
203
+ - run: uv run --no-project pytest -q
204
+
205
+ wheels:
206
+ name: wheel (${{ matrix.os }} ${{ matrix.target }})
207
+ needs: [resolve, verify]
208
+ if: ${{ !cancelled() && needs.verify.result == 'success' }}
209
+ runs-on: ${{ matrix.runner }}
210
+ strategy:
211
+ matrix:
212
+ # Every target on a runner of its own architecture, so the wheel that is
213
+ # uploaded is also the wheel that is tested below.
214
+ include:
215
+ - { os: linux, runner: ubuntu-latest, target: x86_64 }
216
+ - { os: linux, runner: ubuntu-24.04-arm, target: aarch64 }
217
+ - { os: macos, runner: macos-latest, target: universal2-apple-darwin }
218
+ steps:
219
+ - uses: actions/checkout@v4
220
+ with:
221
+ ref: ${{ needs.resolve.outputs.ref }}
222
+ # abi3-py310, so one wheel per platform serves every Python 3.10+.
223
+ # `manylinux` is ignored on macOS.
224
+ - uses: PyO3/maturin-action@v1
225
+ with:
226
+ target: ${{ matrix.target }}
227
+ args: --release --out dist
228
+ manylinux: auto
229
+ # The artifact itself, installed clean and run against the tests -- not a
230
+ # rebuild of the same sources.
231
+ - uses: astral-sh/setup-uv@v6
232
+ - name: test the wheel that is about to be published
233
+ run: |
234
+ uv venv --python 3.12 "$RUNNER_TEMP/fresh"
235
+ uv pip install --python "$RUNNER_TEMP/fresh" dist/*.whl pytest hypothesis pyyaml
236
+ # Run from a directory without python/openodd, so the installed wheel is
237
+ # what gets imported.
238
+ cp -r tests "$RUNNER_TEMP/tests"
239
+ cd "$RUNNER_TEMP" && "$RUNNER_TEMP/fresh/bin/python" -m pytest tests -q
240
+ - uses: actions/upload-artifact@v4
241
+ with:
242
+ name: wheel-${{ matrix.os }}-${{ matrix.target }}
243
+ path: dist
244
+
245
+ sdist:
246
+ name: sdist
247
+ needs: [resolve, verify]
248
+ if: ${{ !cancelled() && needs.verify.result == 'success' }}
249
+ runs-on: ubuntu-latest
250
+ steps:
251
+ - uses: actions/checkout@v4
252
+ with:
253
+ ref: ${{ needs.resolve.outputs.ref }}
254
+ - uses: PyO3/maturin-action@v1
255
+ with:
256
+ command: sdist
257
+ args: --out dist
258
+ - uses: actions/upload-artifact@v4
259
+ with:
260
+ name: sdist
261
+ path: dist
262
+
263
+ # The GitHub release, created for an auto-cut and a hand-cut tag alike, once the
264
+ # wheels have passed -- so a release page never exists without its files.
265
+ github-release:
266
+ name: GitHub release with the wheels attached
267
+ needs: [resolve, wheels, sdist]
268
+ if: ${{ !cancelled() && needs.wheels.result == 'success' && needs.sdist.result == 'success' && needs.resolve.outputs.publish == 'true' }}
269
+ runs-on: ubuntu-latest
270
+ permissions:
271
+ contents: write
272
+ steps:
273
+ - uses: actions/download-artifact@v4
274
+ with:
275
+ pattern: wheel-*
276
+ path: dist
277
+ merge-multiple: true
278
+ - uses: actions/download-artifact@v4
279
+ with:
280
+ name: sdist
281
+ path: dist
282
+ - uses: softprops/action-gh-release@v2
283
+ with:
284
+ tag_name: ${{ needs.resolve.outputs.ref }}
285
+ files: dist/*
286
+ generate_release_notes: true
287
+
288
+ publish:
289
+ name: publish to PyPI
290
+ needs: [resolve, wheels, sdist]
291
+ if: ${{ !cancelled() && needs.wheels.result == 'success' && needs.sdist.result == 'success' && needs.resolve.outputs.publish == 'true' }}
292
+ runs-on: ubuntu-latest
293
+ environment:
294
+ name: pypi
295
+ url: https://pypi.org/p/openodd-py
296
+ permissions:
297
+ id-token: write # Trusted Publishing; no token in secrets.
298
+ steps:
299
+ # Named explicitly rather than "everything in the run", so nothing else a later
300
+ # job uploads can end up in dist/.
301
+ - uses: actions/download-artifact@v4
302
+ with:
303
+ pattern: wheel-*
304
+ path: dist
305
+ merge-multiple: true
306
+ - uses: actions/download-artifact@v4
307
+ with:
308
+ name: sdist
309
+ path: dist
310
+ - run: ls -l dist
311
+
312
+ # PyPI rejects bad metadata only after the build and the OIDC exchange; failing
313
+ # here costs nothing.
314
+ - name: check the metadata before uploading
315
+ run: pipx run twine check --strict dist/*
316
+
317
+ # PyPI refuses to replace a file but accepts new ones for an existing release,
318
+ # so skipping what is already there lets a re-run add what an earlier attempt
319
+ # failed to upload.
320
+ - uses: pypa/gh-action-pypi-publish@release/v1
321
+ with:
322
+ skip-existing: true
@@ -0,0 +1,7 @@
1
+ /target
2
+ /.venv
3
+ __pycache__/
4
+ *.so
5
+ *.pyd
6
+ /dist
7
+ .pytest_cache/