constutil 1.0.0__tar.gz → 1.2.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.
- {constutil-1.0.0 → constutil-1.2.0}/.github/workflows/ci.yml +8 -8
- {constutil-1.0.0 → constutil-1.2.0}/.github/workflows/pages.yml +8 -7
- constutil-1.2.0/.github/workflows/prepare_release.yml +69 -0
- {constutil-1.0.0 → constutil-1.2.0}/.github/workflows/pypi_publish.yml +14 -23
- {constutil-1.0.0 → constutil-1.2.0}/PKG-INFO +96 -19
- {constutil-1.0.0 → constutil-1.2.0}/README.md +94 -15
- {constutil-1.0.0 → constutil-1.2.0}/pyproject.toml +9 -6
- {constutil-1.0.0 → constutil-1.2.0}/scripts/build_docs.py +6 -2
- constutil-1.2.0/scripts/bump-version.sh +43 -0
- constutil-1.2.0/scripts/install-skills.sh +80 -0
- {constutil-1.0.0 → constutil-1.2.0}/src/constutil/constgroup.py +17 -1
- {constutil-1.0.0 → constutil-1.2.0}/tests/test_constutil.py +24 -0
- constutil-1.2.0/tests/test_install_skills.py +71 -0
- {constutil-1.0.0 → constutil-1.2.0}/uv.lock +3 -232
- {constutil-1.0.0 → constutil-1.2.0}/.gitignore +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/LICENSE +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/examples/README.md +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/examples/days.py +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/examples/saturn_moons.py +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/skills/constutil/SKILL.md +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/skills/constutil/agents/openai.yaml +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/src/constutil/__init__.py +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/src/constutil/constdef.py +0 -0
- {constutil-1.0.0 → constutil-1.2.0}/src/constutil/py.typed +0 -0
|
@@ -11,27 +11,27 @@ permissions:
|
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
13
13
|
tests:
|
|
14
|
-
runs-on: ubuntu-
|
|
14
|
+
runs-on: ubuntu-24.04
|
|
15
15
|
strategy:
|
|
16
16
|
fail-fast: false
|
|
17
17
|
matrix:
|
|
18
|
-
python: ['3.
|
|
18
|
+
python: ['3.12', '3.13', '3.14']
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
21
|
-
- uses: astral-sh/setup-uv@
|
|
20
|
+
- uses: actions/checkout@v7.0.1
|
|
21
|
+
- uses: astral-sh/setup-uv@v10.1.0
|
|
22
22
|
with:
|
|
23
23
|
enable-cache: true
|
|
24
24
|
python-version: ${{ matrix.python }}
|
|
25
25
|
- run: uv sync --locked
|
|
26
26
|
- run: uv run pytest --cov=constutil --cov-report=term-missing --cov-fail-under=95
|
|
27
27
|
quality:
|
|
28
|
-
runs-on: ubuntu-
|
|
28
|
+
runs-on: ubuntu-24.04
|
|
29
29
|
steps:
|
|
30
|
-
- uses: actions/checkout@
|
|
31
|
-
- uses: astral-sh/setup-uv@
|
|
30
|
+
- uses: actions/checkout@v7.0.1
|
|
31
|
+
- uses: astral-sh/setup-uv@v10.1.0
|
|
32
32
|
with:
|
|
33
33
|
enable-cache: true
|
|
34
|
-
python-version: '3.
|
|
34
|
+
python-version: '3.12'
|
|
35
35
|
- run: uv sync --locked
|
|
36
36
|
- run: uv run ruff check .
|
|
37
37
|
- run: uv run ruff format --check .
|
|
@@ -14,22 +14,23 @@ concurrency:
|
|
|
14
14
|
|
|
15
15
|
jobs:
|
|
16
16
|
build:
|
|
17
|
-
runs-on: ubuntu-
|
|
17
|
+
runs-on: ubuntu-24.04
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
20
|
-
- uses: astral-sh/setup-uv@
|
|
19
|
+
- uses: actions/checkout@v7.0.1
|
|
20
|
+
- uses: astral-sh/setup-uv@v10.1.0
|
|
21
21
|
with:
|
|
22
22
|
enable-cache: true
|
|
23
23
|
python-version: '3.14'
|
|
24
24
|
- run: uv sync --locked
|
|
25
25
|
- run: uv run python scripts/build_docs.py
|
|
26
|
-
- uses: actions/configure-pages@
|
|
27
|
-
- uses: actions/upload-pages-artifact@
|
|
26
|
+
- uses: actions/configure-pages@v6.0.0
|
|
27
|
+
- uses: actions/upload-pages-artifact@v5.0.0
|
|
28
28
|
with:
|
|
29
29
|
path: site/
|
|
30
|
+
include-hidden-files: true
|
|
30
31
|
deploy:
|
|
31
32
|
needs: build
|
|
32
|
-
runs-on: ubuntu-
|
|
33
|
+
runs-on: ubuntu-24.04
|
|
33
34
|
permissions:
|
|
34
35
|
pages: write
|
|
35
36
|
id-token: write
|
|
@@ -39,4 +40,4 @@ jobs:
|
|
|
39
40
|
steps:
|
|
40
41
|
- name: Deploy
|
|
41
42
|
id: deployment
|
|
42
|
-
uses: actions/deploy-pages@
|
|
43
|
+
uses: actions/deploy-pages@v5.0.1
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Prepare GitHub release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
tag:
|
|
7
|
+
description: Release tag matching project.version (for example v1.0.1)
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: prepare-release-${{ inputs.tag }}
|
|
16
|
+
cancel-in-progress: false
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
test:
|
|
20
|
+
uses: ./.github/workflows/ci.yml
|
|
21
|
+
prepare:
|
|
22
|
+
needs: test
|
|
23
|
+
runs-on: ubuntu-24.04
|
|
24
|
+
permissions:
|
|
25
|
+
contents: write
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v7.0.1
|
|
28
|
+
with:
|
|
29
|
+
ref: ${{ github.sha }}
|
|
30
|
+
- uses: astral-sh/setup-uv@v10.1.0
|
|
31
|
+
with:
|
|
32
|
+
python-version: '3.14'
|
|
33
|
+
- run: uv sync --locked
|
|
34
|
+
- name: Verify tag and version
|
|
35
|
+
env:
|
|
36
|
+
RELEASE_TAG: ${{ inputs.tag }}
|
|
37
|
+
run: |
|
|
38
|
+
uv run python - <<'PY'
|
|
39
|
+
import os
|
|
40
|
+
import tomllib
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
version = tomllib.loads(Path('pyproject.toml').read_text())['project']['version']
|
|
43
|
+
if os.environ['RELEASE_TAG'] != f'v{version}':
|
|
44
|
+
raise SystemExit(f'Release tag must be v{version}')
|
|
45
|
+
PY
|
|
46
|
+
- run: uv build
|
|
47
|
+
- run: uv run twine check dist/*
|
|
48
|
+
- uses: actions/upload-artifact@v7.0.1
|
|
49
|
+
with:
|
|
50
|
+
name: distributions
|
|
51
|
+
path: dist/
|
|
52
|
+
if-no-files-found: error
|
|
53
|
+
- name: Create tag at the tested commit
|
|
54
|
+
env:
|
|
55
|
+
GH_TOKEN: ${{ github.token }}
|
|
56
|
+
GH_REPO: ${{ github.repository }}
|
|
57
|
+
RELEASE_TAG: ${{ inputs.tag }}
|
|
58
|
+
RELEASE_SHA: ${{ github.sha }}
|
|
59
|
+
run: |
|
|
60
|
+
gh api "repos/$GH_REPO/git/refs" --method POST \
|
|
61
|
+
-f "ref=refs/tags/$RELEASE_TAG" -f "sha=$RELEASE_SHA"
|
|
62
|
+
- name: Create draft with package downloads
|
|
63
|
+
env:
|
|
64
|
+
GH_TOKEN: ${{ github.token }}
|
|
65
|
+
GH_REPO: ${{ github.repository }}
|
|
66
|
+
RELEASE_TAG: ${{ inputs.tag }}
|
|
67
|
+
run: |
|
|
68
|
+
gh release create "$RELEASE_TAG" dist/* --draft --verify-tag \
|
|
69
|
+
--title "constutil $RELEASE_TAG" --generate-notes
|
|
@@ -12,10 +12,10 @@ jobs:
|
|
|
12
12
|
uses: ./.github/workflows/ci.yml
|
|
13
13
|
build:
|
|
14
14
|
needs: test
|
|
15
|
-
runs-on: ubuntu-
|
|
15
|
+
runs-on: ubuntu-24.04
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
18
|
-
- uses: astral-sh/setup-uv@
|
|
17
|
+
- uses: actions/checkout@v7.0.1
|
|
18
|
+
- uses: astral-sh/setup-uv@v10.1.0
|
|
19
19
|
with:
|
|
20
20
|
python-version: '3.14'
|
|
21
21
|
- run: uv sync --locked
|
|
@@ -31,41 +31,32 @@ jobs:
|
|
|
31
31
|
if os.environ['RELEASE_TAG'] != f'v{version}':
|
|
32
32
|
raise SystemExit(f'Release tag must be v{version}')
|
|
33
33
|
PY
|
|
34
|
-
-
|
|
34
|
+
- name: Download the immutable release distributions
|
|
35
|
+
env:
|
|
36
|
+
GH_TOKEN: ${{ github.token }}
|
|
37
|
+
GH_REPO: ${{ github.repository }}
|
|
38
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
39
|
+
run: |
|
|
40
|
+
gh release download "$RELEASE_TAG" --pattern '*.whl' --dir dist
|
|
41
|
+
gh release download "$RELEASE_TAG" --pattern '*.tar.gz' --dir dist
|
|
35
42
|
- run: uv run twine check dist/*
|
|
36
|
-
- uses: actions/upload-artifact@
|
|
43
|
+
- uses: actions/upload-artifact@v7.0.1
|
|
37
44
|
with:
|
|
38
45
|
name: distributions
|
|
39
46
|
path: dist/
|
|
40
47
|
if-no-files-found: error
|
|
41
48
|
publish:
|
|
42
49
|
needs: build
|
|
43
|
-
runs-on: ubuntu-
|
|
50
|
+
runs-on: ubuntu-24.04
|
|
44
51
|
environment:
|
|
45
52
|
name: pypi
|
|
46
53
|
url: https://pypi.org/project/constutil/
|
|
47
54
|
permissions:
|
|
48
55
|
id-token: write
|
|
49
56
|
steps:
|
|
50
|
-
- uses: actions/download-artifact@
|
|
57
|
+
- uses: actions/download-artifact@v8.0.1
|
|
51
58
|
with:
|
|
52
59
|
name: distributions
|
|
53
60
|
path: dist/
|
|
54
61
|
- name: Publish with Trusted Publishing
|
|
55
62
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
-
release-assets:
|
|
57
|
-
needs: build
|
|
58
|
-
runs-on: ubuntu-latest
|
|
59
|
-
permissions:
|
|
60
|
-
contents: write
|
|
61
|
-
steps:
|
|
62
|
-
- uses: actions/download-artifact@v4
|
|
63
|
-
with:
|
|
64
|
-
name: distributions
|
|
65
|
-
path: dist/
|
|
66
|
-
- name: Attach distributions to the GitHub release
|
|
67
|
-
env:
|
|
68
|
-
GH_TOKEN: ${{ github.token }}
|
|
69
|
-
GH_REPO: ${{ github.repository }}
|
|
70
|
-
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
71
|
-
run: gh release upload "$RELEASE_TAG" dist/* --clobber
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: constutil
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Typed constant definitions and groups for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/patchfork/constutil
|
|
6
6
|
Project-URL: Documentation, https://constutil.patchfork.dev/
|
|
@@ -13,13 +13,11 @@ Keywords: constants,dataclasses,enum,typing
|
|
|
13
13
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
19
|
Classifier: Typing :: Typed
|
|
22
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.12
|
|
23
21
|
Description-Content-Type: text/markdown
|
|
24
22
|
|
|
25
23
|
# constutil
|
|
@@ -32,13 +30,13 @@ choices or richer records without requiring Python's `enum.Enum`.
|
|
|
32
30
|
|
|
33
31
|
## Installation
|
|
34
32
|
|
|
35
|
-
Requires **Python 3.
|
|
33
|
+
Requires **Python 3.12 or newer**.
|
|
36
34
|
|
|
37
35
|
```sh
|
|
38
36
|
pip install constutil
|
|
39
37
|
```
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
To use changes not yet published to PyPI, install directly from GitHub:
|
|
42
40
|
|
|
43
41
|
```sh
|
|
44
42
|
pip install git+https://github.com/patchfork/constutil.git
|
|
@@ -121,6 +119,12 @@ class Season(StrConstGroup):
|
|
|
121
119
|
|
|
122
120
|
|
|
123
121
|
assert Season.get_value("Spring") == "spring"
|
|
122
|
+
assert Season.get_value_map() == {
|
|
123
|
+
"SPRING": "spring",
|
|
124
|
+
"SUMMER": "summer",
|
|
125
|
+
"AUTUMN": "autumn",
|
|
126
|
+
"WINTER": "winter",
|
|
127
|
+
}
|
|
124
128
|
assert Season.is_valid_value("SPRING") is False
|
|
125
129
|
```
|
|
126
130
|
|
|
@@ -194,6 +198,7 @@ returned as-is and is not required to belong to the group; absent defaults are
|
|
|
194
198
|
| `get_default()` | Configured member or `None` |
|
|
195
199
|
| `get_all()` | Tuple of members |
|
|
196
200
|
| `get_all_map()` | Fresh attribute-name → member dictionary |
|
|
201
|
+
| `get_value_map()` | Fresh attribute-name → stored-value dictionary, in declaration order |
|
|
197
202
|
| `get_all_values()` | Tuple of stored values |
|
|
198
203
|
| `get_all_names()` | Tuple of display names |
|
|
199
204
|
| `get_all_constant_names()` | Tuple of Python attribute names |
|
|
@@ -224,7 +229,7 @@ arguments at runtime.
|
|
|
224
229
|
|
|
225
230
|
## Python compatibility
|
|
226
231
|
|
|
227
|
-
The minimum is **Python 3.
|
|
232
|
+
The minimum is **Python 3.12**, determined by the features actually used:
|
|
228
233
|
|
|
229
234
|
| Feature | Introduced |
|
|
230
235
|
| --- | --- |
|
|
@@ -234,6 +239,7 @@ The minimum is **Python 3.10**, determined by the features actually used:
|
|
|
234
239
|
| Built-in collection annotations such as `tuple[str, ...]` | Python 3.9 |
|
|
235
240
|
| Union annotations such as `MemberT | None` | Python 3.10 |
|
|
236
241
|
| `@dataclass(slots=True)` in optional metadata subclasses | Python 3.10 |
|
|
242
|
+
| `types.get_original_bases()` | Python 3.12 |
|
|
237
243
|
|
|
238
244
|
The generic base deliberately omits `slots=True`: older Python versions raise a
|
|
239
245
|
`TypeError` when instantiating a frozen, slotted generic alias because `typing`
|
|
@@ -241,12 +247,13 @@ tries to assign `__orig_class__`. Frozen definitions without slots work across t
|
|
|
241
247
|
supported versions. A subclass may use slots, but still inherits the base instance
|
|
242
248
|
dictionary.
|
|
243
249
|
|
|
244
|
-
Generic discovery uses
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
[
|
|
249
|
-
|
|
250
|
+
Generic discovery uses the public `types.get_original_bases()` API, introduced in
|
|
251
|
+
Python 3.12, to inspect generic bases before type erasure. This sets the minimum
|
|
252
|
+
Python version; no fallback to direct `__orig_bases__` access is needed. The
|
|
253
|
+
package includes `py.typed`, and CI tests Python 3.12–3.14. See the official
|
|
254
|
+
[generic base introspection documentation](https://docs.python.org/3.12/library/types.html#types.get_original_bases).
|
|
255
|
+
|
|
256
|
+
Version 1.0.0 supports Python 3.10–3.14; version 1.1.0 requires Python 3.12+.
|
|
250
257
|
|
|
251
258
|
## Adopt the coding skill (Codex and Claude Code)
|
|
252
259
|
|
|
@@ -256,6 +263,35 @@ It directs an agent to use `constutil` for related constant values, usually in a
|
|
|
256
263
|
`constants/` package, and explains naming, access, lookup, and existence checks.
|
|
257
264
|
Installing the Python dependency alone does **not** install the skill.
|
|
258
265
|
|
|
266
|
+
### Quick installer
|
|
267
|
+
|
|
268
|
+
From your project's root, download and run the installer:
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
curl -fsSLo install-skills.sh https://constutil.patchfork.dev/install-skills.sh
|
|
272
|
+
sh install-skills.sh both # Codex and Claude Code
|
|
273
|
+
# Or: sh install-skills.sh codex
|
|
274
|
+
# Or: sh install-skills.sh claude
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
From a checkout of this repository, run `sh scripts/install-skills.sh both`.
|
|
278
|
+
The installer uses the checkout's skill files when available; the downloaded
|
|
279
|
+
script fetches them from this site. It installs into `.agents/skills/constutil`
|
|
280
|
+
and/or `.claude/skills/constutil` in the current directory. Use `--global` for
|
|
281
|
+
`~/.agents/skills/constutil` and/or `~/.claude/skills/constutil`. Existing skill
|
|
282
|
+
folders are preserved unless you pass `--force` to update the supplied files.
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
sh install-skills.sh both --global
|
|
286
|
+
sh install-skills.sh both --force
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The script requires `sh` and standard Unix tools, plus `curl` when downloading.
|
|
290
|
+
It does not change `AGENTS.md` or `CLAUDE.md`; add the project convention below
|
|
291
|
+
if you want the skill's guidance to apply consistently.
|
|
292
|
+
|
|
293
|
+
### Manual installation
|
|
294
|
+
|
|
259
295
|
For one project, copy the complete `skills/constutil/` directory from this
|
|
260
296
|
repository to `<your-project>/.agents/skills/constutil/` and commit it. From that
|
|
261
297
|
project's root, with this repository cloned alongside it:
|
|
@@ -352,10 +388,10 @@ and website share one source.
|
|
|
352
388
|
### PyPI
|
|
353
389
|
|
|
354
390
|
The `pypi_publish.yml` workflow runs on a published GitHub release, tests the package
|
|
355
|
-
on Python 3.
|
|
356
|
-
matches the package version,
|
|
357
|
-
|
|
358
|
-
|
|
391
|
+
on Python 3.12–3.14, checks types and formatting, verifies that the release tag
|
|
392
|
+
matches the package version, downloads the wheel and source distribution already
|
|
393
|
+
attached to that release, checks their metadata, and publishes those exact files
|
|
394
|
+
via PyPI Trusted Publishing. It does not require an API token.
|
|
359
395
|
|
|
360
396
|
One-time setup:
|
|
361
397
|
|
|
@@ -363,8 +399,49 @@ One-time setup:
|
|
|
363
399
|
2. On PyPI, configure a pending publisher for `constutil` (or a trusted publisher
|
|
364
400
|
if you already own the project): owner `patchfork`, repository `constutil`,
|
|
365
401
|
workflow filename `pypi_publish.yml`, environment `pypi`.
|
|
366
|
-
|
|
367
|
-
|
|
402
|
+
|
|
403
|
+
#### Trigger a release
|
|
404
|
+
|
|
405
|
+
First bump the package version from the repository root:
|
|
406
|
+
|
|
407
|
+
```sh
|
|
408
|
+
sh scripts/bump-version.sh --dry-run patch # Preview the next patch version
|
|
409
|
+
sh scripts/bump-version.sh patch # Or: minor, major, or an explicit 1.2.0
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The script requires `uv` with the `uv version` command and updates
|
|
413
|
+
`project.version` in `pyproject.toml` and the package entry in `uv.lock`, without
|
|
414
|
+
syncing the environment. Review and commit those changes, then push them to `main`.
|
|
415
|
+
It does not create a commit, tag, or release. Historical version references in
|
|
416
|
+
documentation are left unchanged; review release-specific prose when preparing a
|
|
417
|
+
release.
|
|
418
|
+
|
|
419
|
+
Then prepare the release:
|
|
420
|
+
|
|
421
|
+
1. Open [Prepare GitHub release](https://github.com/patchfork/constutil/actions/workflows/prepare_release.yml).
|
|
422
|
+
2. Click **Run workflow**, select the `main` branch, enter the package version
|
|
423
|
+
prefixed with `v` as the tag (for example, **`v1.2.0`**), and start the workflow.
|
|
424
|
+
3. Wait for the workflow to pass CI and create a **draft release** with the wheel
|
|
425
|
+
and source archive attached.
|
|
426
|
+
4. Open [Releases](https://github.com/patchfork/constutil/releases), review the
|
|
427
|
+
draft's notes and downloads, and click **Publish release**.
|
|
428
|
+
5. Publishing automatically triggers
|
|
429
|
+
[Publish to PyPI](https://github.com/patchfork/constutil/actions/workflows/pypi_publish.yml)
|
|
430
|
+
to upload those exact packages. Check that workflow for the publishing result.
|
|
431
|
+
|
|
432
|
+
The preparation workflow deliberately leaves publication to the user: events
|
|
433
|
+
created using `GITHUB_TOKEN` do not automatically trigger other workflows.
|
|
434
|
+
|
|
435
|
+
GitHub releases are immutable after publication. Uploads must happen while the
|
|
436
|
+
release is still a draft; the publish workflow never adds or replaces release
|
|
437
|
+
assets. A preparation attempt for an existing tag fails rather than moving the tag
|
|
438
|
+
or overwriting a release. If a draft's upload failed, attach the checked build
|
|
439
|
+
artifacts to that draft before publishing it.
|
|
440
|
+
|
|
441
|
+
The original `v1.0.0` release was published without binary attachments and cannot
|
|
442
|
+
be retrofitted. Its packages are available from PyPI; subsequent releases use the
|
|
443
|
+
draft-first process above. Rerunning the historical workflow uses the old workflow
|
|
444
|
+
stored at its tag and cannot apply this fix retroactively.
|
|
368
445
|
|
|
369
446
|
PyPI project-name availability is decided by PyPI when registering or publishing.
|
|
370
447
|
See [PyPI's Trusted Publishing guide](https://docs.pypi.org/trusted-publishers/).
|
|
@@ -8,13 +8,13 @@ choices or richer records without requiring Python's `enum.Enum`.
|
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
Requires **Python 3.
|
|
11
|
+
Requires **Python 3.12 or newer**.
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
14
|
pip install constutil
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
To use changes not yet published to PyPI, install directly from GitHub:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
20
|
pip install git+https://github.com/patchfork/constutil.git
|
|
@@ -97,6 +97,12 @@ class Season(StrConstGroup):
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
assert Season.get_value("Spring") == "spring"
|
|
100
|
+
assert Season.get_value_map() == {
|
|
101
|
+
"SPRING": "spring",
|
|
102
|
+
"SUMMER": "summer",
|
|
103
|
+
"AUTUMN": "autumn",
|
|
104
|
+
"WINTER": "winter",
|
|
105
|
+
}
|
|
100
106
|
assert Season.is_valid_value("SPRING") is False
|
|
101
107
|
```
|
|
102
108
|
|
|
@@ -170,6 +176,7 @@ returned as-is and is not required to belong to the group; absent defaults are
|
|
|
170
176
|
| `get_default()` | Configured member or `None` |
|
|
171
177
|
| `get_all()` | Tuple of members |
|
|
172
178
|
| `get_all_map()` | Fresh attribute-name → member dictionary |
|
|
179
|
+
| `get_value_map()` | Fresh attribute-name → stored-value dictionary, in declaration order |
|
|
173
180
|
| `get_all_values()` | Tuple of stored values |
|
|
174
181
|
| `get_all_names()` | Tuple of display names |
|
|
175
182
|
| `get_all_constant_names()` | Tuple of Python attribute names |
|
|
@@ -200,7 +207,7 @@ arguments at runtime.
|
|
|
200
207
|
|
|
201
208
|
## Python compatibility
|
|
202
209
|
|
|
203
|
-
The minimum is **Python 3.
|
|
210
|
+
The minimum is **Python 3.12**, determined by the features actually used:
|
|
204
211
|
|
|
205
212
|
| Feature | Introduced |
|
|
206
213
|
| --- | --- |
|
|
@@ -210,6 +217,7 @@ The minimum is **Python 3.10**, determined by the features actually used:
|
|
|
210
217
|
| Built-in collection annotations such as `tuple[str, ...]` | Python 3.9 |
|
|
211
218
|
| Union annotations such as `MemberT | None` | Python 3.10 |
|
|
212
219
|
| `@dataclass(slots=True)` in optional metadata subclasses | Python 3.10 |
|
|
220
|
+
| `types.get_original_bases()` | Python 3.12 |
|
|
213
221
|
|
|
214
222
|
The generic base deliberately omits `slots=True`: older Python versions raise a
|
|
215
223
|
`TypeError` when instantiating a frozen, slotted generic alias because `typing`
|
|
@@ -217,12 +225,13 @@ tries to assign `__orig_class__`. Frozen definitions without slots work across t
|
|
|
217
225
|
supported versions. A subclass may use slots, but still inherits the base instance
|
|
218
226
|
dictionary.
|
|
219
227
|
|
|
220
|
-
Generic discovery uses
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
[
|
|
225
|
-
|
|
228
|
+
Generic discovery uses the public `types.get_original_bases()` API, introduced in
|
|
229
|
+
Python 3.12, to inspect generic bases before type erasure. This sets the minimum
|
|
230
|
+
Python version; no fallback to direct `__orig_bases__` access is needed. The
|
|
231
|
+
package includes `py.typed`, and CI tests Python 3.12–3.14. See the official
|
|
232
|
+
[generic base introspection documentation](https://docs.python.org/3.12/library/types.html#types.get_original_bases).
|
|
233
|
+
|
|
234
|
+
Version 1.0.0 supports Python 3.10–3.14; version 1.1.0 requires Python 3.12+.
|
|
226
235
|
|
|
227
236
|
## Adopt the coding skill (Codex and Claude Code)
|
|
228
237
|
|
|
@@ -232,6 +241,35 @@ It directs an agent to use `constutil` for related constant values, usually in a
|
|
|
232
241
|
`constants/` package, and explains naming, access, lookup, and existence checks.
|
|
233
242
|
Installing the Python dependency alone does **not** install the skill.
|
|
234
243
|
|
|
244
|
+
### Quick installer
|
|
245
|
+
|
|
246
|
+
From your project's root, download and run the installer:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
curl -fsSLo install-skills.sh https://constutil.patchfork.dev/install-skills.sh
|
|
250
|
+
sh install-skills.sh both # Codex and Claude Code
|
|
251
|
+
# Or: sh install-skills.sh codex
|
|
252
|
+
# Or: sh install-skills.sh claude
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
From a checkout of this repository, run `sh scripts/install-skills.sh both`.
|
|
256
|
+
The installer uses the checkout's skill files when available; the downloaded
|
|
257
|
+
script fetches them from this site. It installs into `.agents/skills/constutil`
|
|
258
|
+
and/or `.claude/skills/constutil` in the current directory. Use `--global` for
|
|
259
|
+
`~/.agents/skills/constutil` and/or `~/.claude/skills/constutil`. Existing skill
|
|
260
|
+
folders are preserved unless you pass `--force` to update the supplied files.
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
sh install-skills.sh both --global
|
|
264
|
+
sh install-skills.sh both --force
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The script requires `sh` and standard Unix tools, plus `curl` when downloading.
|
|
268
|
+
It does not change `AGENTS.md` or `CLAUDE.md`; add the project convention below
|
|
269
|
+
if you want the skill's guidance to apply consistently.
|
|
270
|
+
|
|
271
|
+
### Manual installation
|
|
272
|
+
|
|
235
273
|
For one project, copy the complete `skills/constutil/` directory from this
|
|
236
274
|
repository to `<your-project>/.agents/skills/constutil/` and commit it. From that
|
|
237
275
|
project's root, with this repository cloned alongside it:
|
|
@@ -328,10 +366,10 @@ and website share one source.
|
|
|
328
366
|
### PyPI
|
|
329
367
|
|
|
330
368
|
The `pypi_publish.yml` workflow runs on a published GitHub release, tests the package
|
|
331
|
-
on Python 3.
|
|
332
|
-
matches the package version,
|
|
333
|
-
|
|
334
|
-
|
|
369
|
+
on Python 3.12–3.14, checks types and formatting, verifies that the release tag
|
|
370
|
+
matches the package version, downloads the wheel and source distribution already
|
|
371
|
+
attached to that release, checks their metadata, and publishes those exact files
|
|
372
|
+
via PyPI Trusted Publishing. It does not require an API token.
|
|
335
373
|
|
|
336
374
|
One-time setup:
|
|
337
375
|
|
|
@@ -339,8 +377,49 @@ One-time setup:
|
|
|
339
377
|
2. On PyPI, configure a pending publisher for `constutil` (or a trusted publisher
|
|
340
378
|
if you already own the project): owner `patchfork`, repository `constutil`,
|
|
341
379
|
workflow filename `pypi_publish.yml`, environment `pypi`.
|
|
342
|
-
|
|
343
|
-
|
|
380
|
+
|
|
381
|
+
#### Trigger a release
|
|
382
|
+
|
|
383
|
+
First bump the package version from the repository root:
|
|
384
|
+
|
|
385
|
+
```sh
|
|
386
|
+
sh scripts/bump-version.sh --dry-run patch # Preview the next patch version
|
|
387
|
+
sh scripts/bump-version.sh patch # Or: minor, major, or an explicit 1.2.0
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The script requires `uv` with the `uv version` command and updates
|
|
391
|
+
`project.version` in `pyproject.toml` and the package entry in `uv.lock`, without
|
|
392
|
+
syncing the environment. Review and commit those changes, then push them to `main`.
|
|
393
|
+
It does not create a commit, tag, or release. Historical version references in
|
|
394
|
+
documentation are left unchanged; review release-specific prose when preparing a
|
|
395
|
+
release.
|
|
396
|
+
|
|
397
|
+
Then prepare the release:
|
|
398
|
+
|
|
399
|
+
1. Open [Prepare GitHub release](https://github.com/patchfork/constutil/actions/workflows/prepare_release.yml).
|
|
400
|
+
2. Click **Run workflow**, select the `main` branch, enter the package version
|
|
401
|
+
prefixed with `v` as the tag (for example, **`v1.2.0`**), and start the workflow.
|
|
402
|
+
3. Wait for the workflow to pass CI and create a **draft release** with the wheel
|
|
403
|
+
and source archive attached.
|
|
404
|
+
4. Open [Releases](https://github.com/patchfork/constutil/releases), review the
|
|
405
|
+
draft's notes and downloads, and click **Publish release**.
|
|
406
|
+
5. Publishing automatically triggers
|
|
407
|
+
[Publish to PyPI](https://github.com/patchfork/constutil/actions/workflows/pypi_publish.yml)
|
|
408
|
+
to upload those exact packages. Check that workflow for the publishing result.
|
|
409
|
+
|
|
410
|
+
The preparation workflow deliberately leaves publication to the user: events
|
|
411
|
+
created using `GITHUB_TOKEN` do not automatically trigger other workflows.
|
|
412
|
+
|
|
413
|
+
GitHub releases are immutable after publication. Uploads must happen while the
|
|
414
|
+
release is still a draft; the publish workflow never adds or replaces release
|
|
415
|
+
assets. A preparation attempt for an existing tag fails rather than moving the tag
|
|
416
|
+
or overwriting a release. If a draft's upload failed, attach the checked build
|
|
417
|
+
artifacts to that draft before publishing it.
|
|
418
|
+
|
|
419
|
+
The original `v1.0.0` release was published without binary attachments and cannot
|
|
420
|
+
be retrofitted. Its packages are available from PyPI; subsequent releases use the
|
|
421
|
+
draft-first process above. Rerunning the historical workflow uses the old workflow
|
|
422
|
+
stored at its tag and cannot apply this fix retroactively.
|
|
344
423
|
|
|
345
424
|
PyPI project-name availability is decided by PyPI when registering or publishing.
|
|
346
425
|
See [PyPI's Trusted Publishing guide](https://docs.pypi.org/trusted-publishers/).
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "constutil"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Typed constant definitions and groups for Python"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [{name = "Patchfork", email = "pypi@patchfork.dev"}]
|
|
@@ -16,8 +16,6 @@ classifiers = [
|
|
|
16
16
|
"Development Status :: 5 - Production/Stable",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
-
"Programming Language :: Python :: 3.10",
|
|
20
|
-
"Programming Language :: Python :: 3.11",
|
|
21
19
|
"Programming Language :: Python :: 3.12",
|
|
22
20
|
"Programming Language :: Python :: 3.13",
|
|
23
21
|
"Programming Language :: Python :: 3.14",
|
|
@@ -42,12 +40,17 @@ addopts = "-ra"
|
|
|
42
40
|
|
|
43
41
|
[tool.ruff]
|
|
44
42
|
line-length = 100
|
|
45
|
-
target-version = "
|
|
43
|
+
target-version = "py312"
|
|
46
44
|
|
|
47
45
|
[tool.ruff.lint]
|
|
48
46
|
select = ["E", "F", "I", "UP"]
|
|
49
47
|
|
|
50
48
|
[tool.mypy]
|
|
51
|
-
python_version = "3.
|
|
49
|
+
python_version = "3.12"
|
|
52
50
|
strict = true
|
|
53
51
|
files = ["src/constutil"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint.per-file-ignores]
|
|
54
|
+
# Retain the existing Generic/TypeVar API while changing base introspection.
|
|
55
|
+
"src/constutil/constdef.py" = ["UP046"]
|
|
56
|
+
"src/constutil/constgroup.py" = ["UP046"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Build the GitHub Pages site from the package README."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from shutil import copytree, make_archive
|
|
4
|
+
from shutil import copyfile, copytree, make_archive
|
|
5
5
|
|
|
6
6
|
import markdown
|
|
7
7
|
|
|
@@ -14,6 +14,7 @@ def main() -> None:
|
|
|
14
14
|
destination = ROOT / "site"
|
|
15
15
|
destination.mkdir(exist_ok=True)
|
|
16
16
|
(destination / ".nojekyll").touch()
|
|
17
|
+
copyfile(ROOT / "scripts" / "install-skills.sh", destination / "install-skills.sh")
|
|
17
18
|
(destination / "index.md").write_text(readme, encoding="utf-8")
|
|
18
19
|
copytree(
|
|
19
20
|
ROOT / "skills" / "constutil", destination / "skills" / "constutil", dirs_exist_ok=True
|
|
@@ -37,7 +38,7 @@ def main() -> None:
|
|
|
37
38
|
(destination / "llms.txt").write_text(
|
|
38
39
|
"""# constutil
|
|
39
40
|
|
|
40
|
-
> Typed constant definitions and groups for Python 3.
|
|
41
|
+
> Typed constant definitions and groups for Python 3.12+, with no runtime dependencies.
|
|
41
42
|
|
|
42
43
|
ConstDef stores a scalar value and display name; ConstGroup provides ordered
|
|
43
44
|
lookup and enumeration. Comparisons use Python equality without coercion or case
|
|
@@ -53,6 +54,9 @@ populated groups. The optional skill recommends a constants package layout.
|
|
|
53
54
|
|
|
54
55
|
## Skills
|
|
55
56
|
|
|
57
|
+
- [Installer](https://constutil.patchfork.dev/install-skills.sh):
|
|
58
|
+
install the shared skill for Codex, Claude Code, or both.
|
|
59
|
+
|
|
56
60
|
- [constutil skill](https://constutil.patchfork.dev/skills/constutil/SKILL.md):
|
|
57
61
|
shared Codex and Claude Code conventions, lookup, and existence checks.
|
|
58
62
|
- [Codex metadata](https://constutil.patchfork.dev/skills/constutil/agents/openai.yaml):
|