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.
Files changed (24) hide show
  1. {constutil-1.0.0 → constutil-1.2.0}/.github/workflows/ci.yml +8 -8
  2. {constutil-1.0.0 → constutil-1.2.0}/.github/workflows/pages.yml +8 -7
  3. constutil-1.2.0/.github/workflows/prepare_release.yml +69 -0
  4. {constutil-1.0.0 → constutil-1.2.0}/.github/workflows/pypi_publish.yml +14 -23
  5. {constutil-1.0.0 → constutil-1.2.0}/PKG-INFO +96 -19
  6. {constutil-1.0.0 → constutil-1.2.0}/README.md +94 -15
  7. {constutil-1.0.0 → constutil-1.2.0}/pyproject.toml +9 -6
  8. {constutil-1.0.0 → constutil-1.2.0}/scripts/build_docs.py +6 -2
  9. constutil-1.2.0/scripts/bump-version.sh +43 -0
  10. constutil-1.2.0/scripts/install-skills.sh +80 -0
  11. {constutil-1.0.0 → constutil-1.2.0}/src/constutil/constgroup.py +17 -1
  12. {constutil-1.0.0 → constutil-1.2.0}/tests/test_constutil.py +24 -0
  13. constutil-1.2.0/tests/test_install_skills.py +71 -0
  14. {constutil-1.0.0 → constutil-1.2.0}/uv.lock +3 -232
  15. {constutil-1.0.0 → constutil-1.2.0}/.gitignore +0 -0
  16. {constutil-1.0.0 → constutil-1.2.0}/LICENSE +0 -0
  17. {constutil-1.0.0 → constutil-1.2.0}/examples/README.md +0 -0
  18. {constutil-1.0.0 → constutil-1.2.0}/examples/days.py +0 -0
  19. {constutil-1.0.0 → constutil-1.2.0}/examples/saturn_moons.py +0 -0
  20. {constutil-1.0.0 → constutil-1.2.0}/skills/constutil/SKILL.md +0 -0
  21. {constutil-1.0.0 → constutil-1.2.0}/skills/constutil/agents/openai.yaml +0 -0
  22. {constutil-1.0.0 → constutil-1.2.0}/src/constutil/__init__.py +0 -0
  23. {constutil-1.0.0 → constutil-1.2.0}/src/constutil/constdef.py +0 -0
  24. {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-latest
14
+ runs-on: ubuntu-24.04
15
15
  strategy:
16
16
  fail-fast: false
17
17
  matrix:
18
- python: ['3.10', '3.11', '3.12', '3.13', '3.14']
18
+ python: ['3.12', '3.13', '3.14']
19
19
  steps:
20
- - uses: actions/checkout@v4
21
- - uses: astral-sh/setup-uv@v6
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-latest
28
+ runs-on: ubuntu-24.04
29
29
  steps:
30
- - uses: actions/checkout@v4
31
- - uses: astral-sh/setup-uv@v6
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.10'
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-latest
17
+ runs-on: ubuntu-24.04
18
18
  steps:
19
- - uses: actions/checkout@v4
20
- - uses: astral-sh/setup-uv@v6
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@v5
27
- - uses: actions/upload-pages-artifact@v3
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-latest
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@v4
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-latest
15
+ runs-on: ubuntu-24.04
16
16
  steps:
17
- - uses: actions/checkout@v4
18
- - uses: astral-sh/setup-uv@v6
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
- - run: uv build
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@v4
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-latest
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@v4
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.0.0
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.10
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.10 or newer**.
33
+ Requires **Python 3.12 or newer**.
36
34
 
37
35
  ```sh
38
36
  pip install constutil
39
37
  ```
40
38
 
41
- Until the first PyPI release, install directly from GitHub:
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.10**, determined by the features actually used:
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 `__orig_bases__`; it does not need Python 3.12's
245
- `types.get_original_bases` or PEP 695 type-parameter syntax. The package includes
246
- `py.typed`. CI tests Python 3.10–3.14. See the official
247
- [dataclass documentation](https://docs.python.org/3.10/library/dataclasses.html),
248
- [typing documentation](https://docs.python.org/3.10/library/typing.html), and
249
- [Python 3.10 changes](https://docs.python.org/3.10/whatsnew/3.10.html).
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.10–3.14, checks types and formatting, verifies that the release tag
356
- matches the package version, builds a wheel and source distribution, checks their
357
- metadata, attaches the same wheel and source distribution to the GitHub release,
358
- and publishes via PyPI Trusted Publishing. It does not require an API token.
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
- 3. Update `project.version` in `pyproject.toml`, run `uv lock`, and commit the changes.
367
- 4. Publish a GitHub release with a matching tag, for example `v1.0.0`.
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.10 or newer**.
11
+ Requires **Python 3.12 or newer**.
12
12
 
13
13
  ```sh
14
14
  pip install constutil
15
15
  ```
16
16
 
17
- Until the first PyPI release, install directly from GitHub:
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.10**, determined by the features actually used:
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 `__orig_bases__`; it does not need Python 3.12's
221
- `types.get_original_bases` or PEP 695 type-parameter syntax. The package includes
222
- `py.typed`. CI tests Python 3.10–3.14. See the official
223
- [dataclass documentation](https://docs.python.org/3.10/library/dataclasses.html),
224
- [typing documentation](https://docs.python.org/3.10/library/typing.html), and
225
- [Python 3.10 changes](https://docs.python.org/3.10/whatsnew/3.10.html).
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.10–3.14, checks types and formatting, verifies that the release tag
332
- matches the package version, builds a wheel and source distribution, checks their
333
- metadata, attaches the same wheel and source distribution to the GitHub release,
334
- and publishes via PyPI Trusted Publishing. It does not require an API token.
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
- 3. Update `project.version` in `pyproject.toml`, run `uv lock`, and commit the changes.
343
- 4. Publish a GitHub release with a matching tag, for example `v1.0.0`.
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.0.0"
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"
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 = "py310"
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.10"
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.10+, with no runtime dependencies.
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):