pyiv 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyiv-0.3.0/.cursor/rules/packaging-docs.mdc +27 -0
- pyiv-0.3.0/.cursor/rules/pydoc-doctest.mdc +17 -0
- pyiv-0.3.0/.cursor/rules/website.mdc +19 -0
- pyiv-0.3.0/.cursor/skills/maintain-docs/SKILL.md +36 -0
- pyiv-0.3.0/.dockerignore +40 -0
- pyiv-0.3.0/.github/workflows/ci.yml +97 -0
- pyiv-0.3.0/.github/workflows/docs.yml +227 -0
- pyiv-0.3.0/.github/workflows/release.yml +143 -0
- pyiv-0.3.0/.github/workflows/version-bump.yml +158 -0
- pyiv-0.3.0/.gitignore +79 -0
- pyiv-0.3.0/.pre-commit-config.yaml +28 -0
- pyiv-0.3.0/AGENTS.md +279 -0
- pyiv-0.3.0/CHANGELOG.md +75 -0
- pyiv-0.3.0/Dockerfile +40 -0
- pyiv-0.3.0/LICENSE +26 -0
- pyiv-0.3.0/Makefile +46 -0
- pyiv-0.3.0/PKG-INFO +101 -0
- pyiv-0.3.0/README.md +61 -0
- pyiv-0.3.0/README_PRE_COMMIT.md +63 -0
- pyiv-0.3.0/RELEASE.md +59 -0
- pyiv-0.3.0/bandit-report.json +172 -0
- pyiv-0.3.0/check_docs_quality.py +587 -0
- pyiv-0.3.0/docker-compose.yml +46 -0
- pyiv-0.3.0/docs/README.md +31 -0
- pyiv-0.3.0/docs/_static/custom.css +78 -0
- pyiv-0.3.0/docs/changelog.rst +2 -0
- pyiv-0.3.0/docs/conf.py +96 -0
- pyiv-0.3.0/docs/guide/binding.rst +71 -0
- pyiv-0.3.0/docs/guide/keys.rst +105 -0
- pyiv-0.3.0/docs/guide/scopes.rst +62 -0
- pyiv-0.3.0/docs/guide/testing.rst +57 -0
- pyiv-0.3.0/docs/index.rst +180 -0
- pyiv-0.3.0/docs/modules.rst +10 -0
- pyiv-0.3.0/docs/pyiv/pyiv.binder.rst +13 -0
- pyiv-0.3.0/docs/pyiv/pyiv.binder_impl.rst +13 -0
- pyiv-0.3.0/docs/pyiv/pyiv.chain.rst +13 -0
- pyiv-0.3.0/docs/pyiv/pyiv.clock.rst +17 -0
- pyiv-0.3.0/docs/pyiv/pyiv.command.rst +15 -0
- pyiv-0.3.0/docs/pyiv/pyiv.config.rst +12 -0
- pyiv-0.3.0/docs/pyiv/pyiv.console.rst +16 -0
- pyiv-0.3.0/docs/pyiv/pyiv.datetime_service.rst +14 -0
- pyiv-0.3.0/docs/pyiv/pyiv.factory.rst +14 -0
- pyiv-0.3.0/docs/pyiv/pyiv.filesystem.rst +14 -0
- pyiv-0.3.0/docs/pyiv/pyiv.injector.rst +18 -0
- pyiv-0.3.0/docs/pyiv/pyiv.key.rst +14 -0
- pyiv-0.3.0/docs/pyiv/pyiv.members.rst +13 -0
- pyiv-0.3.0/docs/pyiv/pyiv.multibinder.rst +14 -0
- pyiv-0.3.0/docs/pyiv/pyiv.network.base.rst +12 -0
- pyiv-0.3.0/docs/pyiv/pyiv.network.clients.rst +13 -0
- pyiv-0.3.0/docs/pyiv/pyiv.network.rst +14 -0
- pyiv-0.3.0/docs/pyiv/pyiv.optional.rst +19 -0
- pyiv-0.3.0/docs/pyiv/pyiv.provider.rst +16 -0
- pyiv-0.3.0/docs/pyiv/pyiv.reflection.rst +12 -0
- pyiv-0.3.0/docs/pyiv/pyiv.rst +4 -0
- pyiv-0.3.0/docs/pyiv/pyiv.scope.rst +15 -0
- pyiv-0.3.0/docs/pyiv/pyiv.serde.base.rst +12 -0
- pyiv-0.3.0/docs/pyiv/pyiv.serde.encodings.rst +17 -0
- pyiv-0.3.0/docs/pyiv/pyiv.serde.json.rst +6 -0
- pyiv-0.3.0/docs/pyiv/pyiv.serde.rst +15 -0
- pyiv-0.3.0/docs/pyiv/pyiv.singleton.rst +13 -0
- pyiv-0.3.0/generate_index.py +353 -0
- pyiv-0.3.0/poetry.lock +831 -0
- pyiv-0.3.0/pyiv/__init__.py +137 -0
- pyiv-0.3.0/pyiv/binder.py +203 -0
- pyiv-0.3.0/pyiv/binder_impl.py +178 -0
- pyiv-0.3.0/pyiv/chain.py +79 -0
- pyiv-0.3.0/pyiv/clock.py +411 -0
- pyiv-0.3.0/pyiv/command.py +581 -0
- pyiv-0.3.0/pyiv/config.py +555 -0
- pyiv-0.3.0/pyiv/console.py +2143 -0
- pyiv-0.3.0/pyiv/datetime_service.py +115 -0
- pyiv-0.3.0/pyiv/factory.py +133 -0
- pyiv-0.3.0/pyiv/filesystem.py +801 -0
- pyiv-0.3.0/pyiv/injector.py +707 -0
- pyiv-0.3.0/pyiv/key.py +223 -0
- pyiv-0.3.0/pyiv/members.py +235 -0
- pyiv-0.3.0/pyiv/multibinder.py +291 -0
- pyiv-0.3.0/pyiv/network/__init__.py +20 -0
- pyiv-0.3.0/pyiv/network/base.py +137 -0
- pyiv-0.3.0/pyiv/network/clients.py +202 -0
- pyiv-0.3.0/pyiv/optional.py +174 -0
- pyiv-0.3.0/pyiv/provider.py +288 -0
- pyiv-0.3.0/pyiv/py.typed +0 -0
- pyiv-0.3.0/pyiv/reflection.py +362 -0
- pyiv-0.3.0/pyiv/scope.py +302 -0
- pyiv-0.3.0/pyiv/serde/__init__.py +47 -0
- pyiv-0.3.0/pyiv/serde/base.py +125 -0
- pyiv-0.3.0/pyiv/serde/encodings.py +373 -0
- pyiv-0.3.0/pyiv/serde/json.py +33 -0
- pyiv-0.3.0/pyiv/singleton.py +121 -0
- pyiv-0.3.0/pyproject.toml +97 -0
- pyiv-0.3.0/run_checks.sh +143 -0
- pyiv-0.3.0/scripts/generate_index.py +352 -0
- pyiv-0.3.0/scripts/style_pydoc_html.py +361 -0
- pyiv-0.3.0/tests/__init__.py +1 -0
- pyiv-0.3.0/tests/conftest.py +48 -0
- pyiv-0.3.0/tests/test_clock.py +163 -0
- pyiv-0.3.0/tests/test_command.py +710 -0
- pyiv-0.3.0/tests/test_console.py +630 -0
- pyiv-0.3.0/tests/test_datetime_service.py +148 -0
- pyiv-0.3.0/tests/test_factory.py +119 -0
- pyiv-0.3.0/tests/test_filesystem.py +129 -0
- pyiv-0.3.0/tests/test_injector.py +189 -0
- pyiv-0.3.0/tests/test_injector_factory_with_injector.py +81 -0
- pyiv-0.3.0/tests/test_key.py +53 -0
- pyiv-0.3.0/tests/test_multibinder.py +61 -0
- pyiv-0.3.0/tests/test_network.py +307 -0
- pyiv-0.3.0/tests/test_reflection.py +676 -0
- pyiv-0.3.0/tests/test_reflection_nested_paths.py +220 -0
- pyiv-0.3.0/tests/test_reflection_re_export.py +157 -0
- pyiv-0.3.0/tests/test_serde.py +387 -0
- pyiv-0.3.0/tests/test_singleton.py +293 -0
- pyiv-0.3.0/trigger-docs.sh +43 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: PyPI metadata, install claims, changelog format, and GitHub release copy must match README and the docs site.
|
|
3
|
+
globs: pyproject.toml,RELEASE.md,CHANGELOG.md,LICENSE,.github/workflows/release.yml,.github/workflows/version-bump.yml
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Packaging copy
|
|
8
|
+
|
|
9
|
+
`[project]` in `pyproject.toml` is the PyPI sidebar. Keep it real:
|
|
10
|
+
|
|
11
|
+
- Author and URLs must not be `Your Name` / `yourusername`.
|
|
12
|
+
- `Documentation` = `https://rl337.org/pyiv/`
|
|
13
|
+
- Homepage / Repository / Issues = `https://github.com/rl337/pyiv` (and `/issues`)
|
|
14
|
+
- `Changelog` = `https://rl337.org/pyiv/changelog.html`
|
|
15
|
+
- Description should match the README one-liner (Guice-style DI, not factory-first)
|
|
16
|
+
|
|
17
|
+
Install commands in `RELEASE.md`, release workflow text, and GitHub Release bodies must match README: `pip install pyiv` (and `pip install pyiv==<version>` on a tagged release). Unreleased `main` may use the git URL. Do not mention Poetry. PyPI is final `X.Y.Z` only.
|
|
18
|
+
|
|
19
|
+
## Changelog
|
|
20
|
+
|
|
21
|
+
`CHANGELOG.md` is the source of truth (GitHub, PyPI URL, Sphinx `docs/changelog.rst`). Keep a Changelog layout:
|
|
22
|
+
|
|
23
|
+
- New user-facing work goes under `## Unreleased`. Do not invent `X.Y.Z` in a PR; versions are auto-bumped on `main`.
|
|
24
|
+
- After that bump is on `main`, move Unreleased bullets to `## X.Y.Z - YYYY-MM-DD`.
|
|
25
|
+
- User-facing only (Added / Changed / Deprecated / Removed / Fixed / Security). No “fix typo in comment,” no squash-commit dumps.
|
|
26
|
+
- One set of bullets GitHub Releases can reuse. Same install command as README.
|
|
27
|
+
- The docs homepage and `project.urls` Changelog must keep pointing at the site page.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Runnable doctests in pyiv module and class docstrings; they are the API docs source of truth.
|
|
3
|
+
globs: pyiv/**/*.py
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Doctests in pyiv
|
|
8
|
+
|
|
9
|
+
Public module and class docstrings are compiled into the Sphinx site and executed by `pytest --doctest-modules`.
|
|
10
|
+
|
|
11
|
+
- Examples must be self-contained: define every name the snippet uses.
|
|
12
|
+
- Do not hit the network, sleep for real time, or write files in the repo cwd. Use in-memory doubles (`MemoryFilesystem`, `SyntheticClock`, `MemoryConsole`) or invalid-input `Traceback` examples.
|
|
13
|
+
- Inject `Set[T]` / `List[T]` through a host class constructor, not `injector.inject(Set[T])`.
|
|
14
|
+
- Unregistered **concrete** classes are instantiated; Optional-none examples need an ABC (or a type the injector cannot build).
|
|
15
|
+
- Expected output containing a newline must use `\\n` in the docstring (otherwise doctest sees a real line break).
|
|
16
|
+
- Do not use `# doctest: +SKIP`. If a method is a variant of another API, put one
|
|
17
|
+
self-contained example on the primary method and point the other at it in prose.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: README vs Sphinx split, install story, homepage features, and RTD sidebar.
|
|
3
|
+
globs: docs/**/*.rst,docs/**/*.md,docs/conf.py,README.md
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Website and README
|
|
8
|
+
|
|
9
|
+
README is the GitHub **and** PyPI long description: what pyiv is, honest install, short quick start, link to https://rl337.org/pyiv/. Do not duplicate the API reference.
|
|
10
|
+
|
|
11
|
+
**docs/** is the product site. Key Features: type-based injection, scopes, keys/binder, reflection, test doubles, zero dependencies. Do not headline Factory.
|
|
12
|
+
|
|
13
|
+
Install must match README and release notes: `pip install pyiv`. Unreleased `main` may use `pip install git+https://github.com/rl337/pyiv.git`. No Poetry.
|
|
14
|
+
|
|
15
|
+
API nav: grouped `toctree`s on `docs/index.rst` (User guide, Core DI, Bindings, Discovery, Test doubles, Integrations). User guide pages live in `docs/guide/` with human titles. Keep `collapse_navigation: False` and `titles_only: True`. Omit `binder_impl`. New public modules need `docs/pyiv/pyiv.<name>.rst` and a toctree entry.
|
|
16
|
+
|
|
17
|
+
Production is https://rl337.org/pyiv/ from `main`. PR previews are `/branch/<branch>/` only.
|
|
18
|
+
|
|
19
|
+
If a changelog page exists, link it from the site (homepage + Reference toctree) and from `project.urls`. `CHANGELOG.md` is the source of truth; `docs/changelog.rst` includes it.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: maintain-docs
|
|
3
|
+
description: Keep pyiv's PyPI listing, Sphinx site, and release notes in sync. Use when editing README.md, docs/, pyproject.toml metadata or URLs, CHANGELOG, GitHub releases, install instructions, Key Features, public APIs, or doctests; when adding modules to the API nav; or when the user mentions the website, PyPI, documentation, or changelog.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Maintain pyiv docs
|
|
7
|
+
|
|
8
|
+
Three public surfaces must agree. Docstrings feed the site; they are not a fourth landing page.
|
|
9
|
+
|
|
10
|
+
| Pillar | Files | Job |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| PyPI listing | `README.md`, `[project]` in `pyproject.toml` | README is GitHub **and** Warehouse long_description. Metadata (author, URLs, description) is the sidebar. Not a second API manual. |
|
|
13
|
+
| Website | `docs/`, `pyiv/**/*.py` docstrings | Product manual at https://rl337.org/pyiv/. Features, install, guide, autodoc. Production is `main` only; PRs go to `/branch/<name>/`. |
|
|
14
|
+
| Release notes | `CHANGELOG.md`, GitHub Releases, `RELEASE.md`, `.github/workflows/release.yml` | Why this version shipped. Same install command as README. Not a squash-commit dump. |
|
|
15
|
+
|
|
16
|
+
## Install story (must match all three)
|
|
17
|
+
|
|
18
|
+
- Install: `pip install pyiv`
|
|
19
|
+
- Docs + PyPI: https://rl337.org/pyiv/ and https://pypi.org/project/pyiv/
|
|
20
|
+
- Unreleased `main`: `pip install git+https://github.com/rl337/pyiv.git`
|
|
21
|
+
- Do not mention Poetry.
|
|
22
|
+
- PyPI is production `X.Y.Z` only (no RCs or nightlies). First public version is **0.3.0**.
|
|
23
|
+
|
|
24
|
+
Canonical URLs (also `[project.urls]`): Homepage/Repository `https://github.com/rl337/pyiv`, Documentation `https://rl337.org/pyiv/`, Changelog `https://rl337.org/pyiv/changelog.html`. No `yourusername` placeholders.
|
|
25
|
+
|
|
26
|
+
## After a public API change
|
|
27
|
+
|
|
28
|
+
1. Runnable module doctest (self-contained; no network, real sleeps, or cwd writes).
|
|
29
|
+
2. New public module: `docs/pyiv/pyiv.<module>.rst` **and** the matching toctree on `docs/index.rst`. Omit `binder_impl`.
|
|
30
|
+
3. Homepage Key Features stay: type injection, scopes, keys/binder, reflection, test doubles, zero deps. Not Factory-first.
|
|
31
|
+
4. If the change is user-visible, add a bullet under `## Unreleased` in `CHANGELOG.md` (do not invent a version). GitHub Releases reuse those bullets.
|
|
32
|
+
5. `pytest --doctest-modules pyiv` and `sphinx-build -b html docs docs/_build/html`.
|
|
33
|
+
|
|
34
|
+
## Sidebar UX
|
|
35
|
+
|
|
36
|
+
RTD only expands nested toctrees from the current page. Grouped `.. toctree::` directives live on `docs/index.rst` (User guide in `docs/guide/`, then API groups). Keep `collapse_navigation: False` and `titles_only: True` in `docs/conf.py`.
|
pyiv-0.3.0/.dockerignore
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
.tox/
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
ENV/
|
|
17
|
+
env/
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
*.swp
|
|
23
|
+
*.swo
|
|
24
|
+
*~
|
|
25
|
+
|
|
26
|
+
# Git
|
|
27
|
+
.git/
|
|
28
|
+
.gitignore
|
|
29
|
+
|
|
30
|
+
# Documentation
|
|
31
|
+
*.md
|
|
32
|
+
!README.md
|
|
33
|
+
|
|
34
|
+
# CI/CD
|
|
35
|
+
.github/
|
|
36
|
+
|
|
37
|
+
# Other
|
|
38
|
+
.DS_Store
|
|
39
|
+
*.log
|
|
40
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master, develop]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
validation:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Cache pip packages
|
|
26
|
+
uses: actions/cache@v4
|
|
27
|
+
with:
|
|
28
|
+
path: ~/.cache/pip
|
|
29
|
+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}
|
|
30
|
+
restore-keys: |
|
|
31
|
+
${{ runner.os }}-pip-${{ matrix.python-version }}-
|
|
32
|
+
${{ runner.os }}-pip-
|
|
33
|
+
|
|
34
|
+
- name: Install build dependencies
|
|
35
|
+
run: |
|
|
36
|
+
python -m pip install --upgrade pip
|
|
37
|
+
pip install build hatchling
|
|
38
|
+
|
|
39
|
+
- name: Install project dependencies
|
|
40
|
+
run: |
|
|
41
|
+
python -m pip install --upgrade pip
|
|
42
|
+
pip install -e ".[dev,docs]" || pip install -e .
|
|
43
|
+
# Pin versions for consistency across Python versions
|
|
44
|
+
# black 25.x requires Python 3.9+, use 24.8.0 for Python 3.8 compatibility
|
|
45
|
+
# isort 7.0.0 may not be available, use 5.13.2 for compatibility
|
|
46
|
+
# mypy 1.8+ requires Python 3.9+, use 1.7.1 for Python 3.8 compatibility
|
|
47
|
+
if python -c "import sys; exit(0 if sys.version_info >= (3, 9) else 1)"; then
|
|
48
|
+
pip install pytest pytest-cov black==25.11.0 isort==5.13.2 mypy bandit
|
|
49
|
+
else
|
|
50
|
+
pip install pytest pytest-cov black==24.8.0 isort==5.13.2 "mypy<1.8" bandit
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
- name: Run validation checks
|
|
54
|
+
run: |
|
|
55
|
+
if [ -f "run_checks.sh" ]; then
|
|
56
|
+
chmod +x run_checks.sh
|
|
57
|
+
./run_checks.sh
|
|
58
|
+
else
|
|
59
|
+
echo "No run_checks.sh found, running basic checks..."
|
|
60
|
+
pytest --cov=pyiv --cov-report=xml --cov-report=html --cov-report=term-missing
|
|
61
|
+
black --check pyiv/ tests/ || true
|
|
62
|
+
isort --check-only pyiv/ tests/ || true
|
|
63
|
+
mypy pyiv/ || true
|
|
64
|
+
bandit -r pyiv/ -f json -o bandit-report.json || true
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
- name: Upload coverage to Codecov
|
|
68
|
+
uses: codecov/codecov-action@v4
|
|
69
|
+
with:
|
|
70
|
+
file: ./build/coverage.xml
|
|
71
|
+
flags: pyiv
|
|
72
|
+
name: codecov-pyiv-${{ matrix.python-version }}
|
|
73
|
+
fail_ci_if_error: false
|
|
74
|
+
|
|
75
|
+
- name: Upload bandit security report
|
|
76
|
+
if: always()
|
|
77
|
+
uses: actions/upload-artifact@v4
|
|
78
|
+
with:
|
|
79
|
+
name: bandit-security-report-${{ matrix.python-version }}
|
|
80
|
+
path: build/bandit-report.json
|
|
81
|
+
if-no-files-found: ignore
|
|
82
|
+
|
|
83
|
+
- name: Upload coverage HTML report
|
|
84
|
+
if: always()
|
|
85
|
+
uses: actions/upload-artifact@v4
|
|
86
|
+
with:
|
|
87
|
+
name: coverage-html-report-${{ matrix.python-version }}
|
|
88
|
+
path: build/htmlcov/
|
|
89
|
+
if-no-files-found: ignore
|
|
90
|
+
|
|
91
|
+
- name: Build distribution packages
|
|
92
|
+
run: |
|
|
93
|
+
python -m pip install --upgrade build twine
|
|
94
|
+
python -m build --sdist --wheel
|
|
95
|
+
twine check dist/*
|
|
96
|
+
echo "✓ Distribution packages built successfully"
|
|
97
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
name: Documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
paths:
|
|
7
|
+
- "pyiv/**"
|
|
8
|
+
- "docs/**"
|
|
9
|
+
- ".github/workflows/docs.yml"
|
|
10
|
+
pull_request:
|
|
11
|
+
types: [opened, synchronize, reopened, closed]
|
|
12
|
+
branches: [main, master]
|
|
13
|
+
paths:
|
|
14
|
+
- "pyiv/**"
|
|
15
|
+
- "docs/**"
|
|
16
|
+
- ".github/workflows/docs.yml"
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
|
|
19
|
+
# One site tree (production root + /branch/<name>/ previews). Serialize deploys
|
|
20
|
+
# so a PR cannot clobber main and two previews cannot race on gh-pages.
|
|
21
|
+
concurrency:
|
|
22
|
+
group: documentation-site
|
|
23
|
+
cancel-in-progress: false
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: write
|
|
27
|
+
pages: write
|
|
28
|
+
id-token: write
|
|
29
|
+
pull-requests: write
|
|
30
|
+
|
|
31
|
+
env:
|
|
32
|
+
PREVIEW_DIR: branch
|
|
33
|
+
SITE_URL: https://rl337.org/pyiv
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
publish:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
# Fork PRs cannot push gh-pages or deploy Pages.
|
|
39
|
+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
|
40
|
+
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Set up Python 3.12
|
|
45
|
+
if: github.event.action != 'closed'
|
|
46
|
+
uses: actions/setup-python@v5
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.12"
|
|
49
|
+
|
|
50
|
+
- name: Cache pip packages
|
|
51
|
+
if: github.event.action != 'closed'
|
|
52
|
+
uses: actions/cache@v4
|
|
53
|
+
with:
|
|
54
|
+
path: ~/.cache/pip
|
|
55
|
+
key: ${{ runner.os }}-pip-3.12-docs-${{ hashFiles('pyproject.toml') }}
|
|
56
|
+
restore-keys: |
|
|
57
|
+
${{ runner.os }}-pip-3.12-docs-
|
|
58
|
+
${{ runner.os }}-pip-3.12-
|
|
59
|
+
|
|
60
|
+
- name: Install documentation dependencies
|
|
61
|
+
if: github.event.action != 'closed'
|
|
62
|
+
run: |
|
|
63
|
+
python -m pip install --upgrade pip
|
|
64
|
+
pip install -e ".[docs]"
|
|
65
|
+
|
|
66
|
+
- name: Build documentation with Sphinx
|
|
67
|
+
if: github.event.action != 'closed'
|
|
68
|
+
run: sphinx-build -b html docs docs/_build/html
|
|
69
|
+
|
|
70
|
+
- name: Upload Sphinx artifact
|
|
71
|
+
if: github.event.action != 'closed'
|
|
72
|
+
uses: actions/upload-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: pyiv-documentation
|
|
75
|
+
path: docs/_build/html
|
|
76
|
+
if-no-files-found: error
|
|
77
|
+
|
|
78
|
+
- name: Checkout published site tree
|
|
79
|
+
continue-on-error: true
|
|
80
|
+
uses: actions/checkout@v4
|
|
81
|
+
with:
|
|
82
|
+
ref: gh-pages
|
|
83
|
+
path: published
|
|
84
|
+
|
|
85
|
+
- name: Assemble production root and branch previews
|
|
86
|
+
id: preview
|
|
87
|
+
env:
|
|
88
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
89
|
+
EVENT_ACTION: ${{ github.event.action }}
|
|
90
|
+
GIT_REF: ${{ github.ref }}
|
|
91
|
+
HEAD_REF: ${{ github.head_ref || github.ref_name }}
|
|
92
|
+
run: |
|
|
93
|
+
set -euo pipefail
|
|
94
|
+
mkdir -p published
|
|
95
|
+
rm -rf published/.git
|
|
96
|
+
|
|
97
|
+
python3 <<'PY'
|
|
98
|
+
import os, re, shutil
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
|
|
101
|
+
published = Path("published")
|
|
102
|
+
published.mkdir(exist_ok=True)
|
|
103
|
+
preview_root = published / os.environ["PREVIEW_DIR"]
|
|
104
|
+
sphinx = Path("docs/_build/html")
|
|
105
|
+
event = os.environ["EVENT_NAME"]
|
|
106
|
+
action = os.environ.get("EVENT_ACTION") or ""
|
|
107
|
+
git_ref = os.environ["GIT_REF"]
|
|
108
|
+
head_ref = os.environ["HEAD_REF"]
|
|
109
|
+
parts = [
|
|
110
|
+
re.sub(r"[^A-Za-z0-9._-]", "-", p)
|
|
111
|
+
for p in head_ref.split("/")
|
|
112
|
+
if p not in ("", ".", "..")
|
|
113
|
+
]
|
|
114
|
+
safe = "/".join(parts) or "unnamed"
|
|
115
|
+
is_prod = git_ref in ("refs/heads/main", "refs/heads/master") and event != "pull_request"
|
|
116
|
+
preview_url = f"{os.environ['SITE_URL']}/{os.environ['PREVIEW_DIR']}/{safe}/"
|
|
117
|
+
|
|
118
|
+
def copy_tree(src: Path, dest: Path) -> None:
|
|
119
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
for item in src.iterdir():
|
|
121
|
+
target = dest / item.name
|
|
122
|
+
if item.is_dir():
|
|
123
|
+
shutil.copytree(item, target, dirs_exist_ok=True)
|
|
124
|
+
else:
|
|
125
|
+
shutil.copy2(item, target)
|
|
126
|
+
|
|
127
|
+
if event == "pull_request" and action == "closed":
|
|
128
|
+
shutil.rmtree(preview_root / safe, ignore_errors=True)
|
|
129
|
+
elif is_prod:
|
|
130
|
+
saved = Path("/tmp/pyiv-preview-branch")
|
|
131
|
+
if preview_root.exists():
|
|
132
|
+
if saved.exists():
|
|
133
|
+
shutil.rmtree(saved)
|
|
134
|
+
shutil.copytree(preview_root, saved)
|
|
135
|
+
for child in list(published.iterdir()):
|
|
136
|
+
if child.name == os.environ["PREVIEW_DIR"]:
|
|
137
|
+
continue
|
|
138
|
+
if child.is_dir():
|
|
139
|
+
shutil.rmtree(child)
|
|
140
|
+
else:
|
|
141
|
+
child.unlink()
|
|
142
|
+
copy_tree(sphinx, published)
|
|
143
|
+
if saved.exists():
|
|
144
|
+
if preview_root.exists():
|
|
145
|
+
shutil.rmtree(preview_root)
|
|
146
|
+
shutil.copytree(saved, preview_root)
|
|
147
|
+
else:
|
|
148
|
+
dest = preview_root / safe
|
|
149
|
+
if dest.exists():
|
|
150
|
+
shutil.rmtree(dest)
|
|
151
|
+
copy_tree(sphinx, dest)
|
|
152
|
+
|
|
153
|
+
(published / ".nojekyll").write_text("")
|
|
154
|
+
github_output = Path(os.environ["GITHUB_OUTPUT"])
|
|
155
|
+
with github_output.open("a") as fh:
|
|
156
|
+
fh.write(f"safe={safe}\n")
|
|
157
|
+
fh.write(f"url={preview_url}\n")
|
|
158
|
+
fh.write(f"is_prod={'true' if is_prod else 'false'}\n")
|
|
159
|
+
fh.write(f"has_root={'true' if (published / 'index.html').is_file() else 'false'}\n")
|
|
160
|
+
PY
|
|
161
|
+
|
|
162
|
+
- name: Publish site tree to gh-pages
|
|
163
|
+
uses: JamesIves/github-pages-deploy-action@v4
|
|
164
|
+
with:
|
|
165
|
+
folder: published
|
|
166
|
+
branch: gh-pages
|
|
167
|
+
clean: true
|
|
168
|
+
|
|
169
|
+
- name: Setup Pages
|
|
170
|
+
if: steps.preview.outputs.has_root == 'true'
|
|
171
|
+
uses: actions/configure-pages@v4
|
|
172
|
+
|
|
173
|
+
- name: Upload Pages artifact
|
|
174
|
+
if: steps.preview.outputs.has_root == 'true'
|
|
175
|
+
uses: actions/upload-pages-artifact@v3
|
|
176
|
+
with:
|
|
177
|
+
path: published
|
|
178
|
+
|
|
179
|
+
- name: Deploy to GitHub Pages
|
|
180
|
+
if: steps.preview.outputs.has_root == 'true'
|
|
181
|
+
id: deployment
|
|
182
|
+
uses: actions/deploy-pages@v4
|
|
183
|
+
with:
|
|
184
|
+
artifact_name: github-pages
|
|
185
|
+
|
|
186
|
+
- name: Comment preview URL on the pull request
|
|
187
|
+
if: github.event_name == 'pull_request'
|
|
188
|
+
uses: actions/github-script@v7
|
|
189
|
+
env:
|
|
190
|
+
PREVIEW_URL: ${{ steps.preview.outputs.url }}
|
|
191
|
+
HAS_ROOT: ${{ steps.preview.outputs.has_root }}
|
|
192
|
+
CLOSED: ${{ github.event.action == 'closed' }}
|
|
193
|
+
with:
|
|
194
|
+
script: |
|
|
195
|
+
const marker = '<!-- pyiv-docs-preview -->';
|
|
196
|
+
const closed = process.env.CLOSED === 'true';
|
|
197
|
+
const live = process.env.HAS_ROOT === 'true';
|
|
198
|
+
const url = process.env.PREVIEW_URL;
|
|
199
|
+
let body;
|
|
200
|
+
if (closed) {
|
|
201
|
+
body = `${marker}\nPreview removed from the docs site. Production is unchanged at ${process.env.SITE_URL}/.`;
|
|
202
|
+
} else if (live) {
|
|
203
|
+
body = `${marker}\nDocs preview: ${url}\n\nProduction stays at ${process.env.SITE_URL}/ (main only). This preview updates on each push.`;
|
|
204
|
+
} else {
|
|
205
|
+
body = `${marker}\nDocs preview will be at ${url} after the next **main** deploy (the production tree is not on \`gh-pages\` yet, so this PR will not publish over the live homepage).`;
|
|
206
|
+
}
|
|
207
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
208
|
+
owner: context.repo.owner,
|
|
209
|
+
repo: context.repo.repo,
|
|
210
|
+
issue_number: context.issue.number,
|
|
211
|
+
});
|
|
212
|
+
const existing = comments.find(c => c.body && c.body.includes(marker));
|
|
213
|
+
if (existing) {
|
|
214
|
+
await github.rest.issues.updateComment({
|
|
215
|
+
owner: context.repo.owner,
|
|
216
|
+
repo: context.repo.repo,
|
|
217
|
+
comment_id: existing.id,
|
|
218
|
+
body,
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
await github.rest.issues.createComment({
|
|
222
|
+
owner: context.repo.owner,
|
|
223
|
+
repo: context.repo.repo,
|
|
224
|
+
issue_number: context.issue.number,
|
|
225
|
+
body,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: ["CI"]
|
|
6
|
+
types:
|
|
7
|
+
- completed
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
- master
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
inputs:
|
|
13
|
+
publish_target:
|
|
14
|
+
description: "Upload destination. none = GitHub Release only (if untagged)."
|
|
15
|
+
required: true
|
|
16
|
+
default: none
|
|
17
|
+
type: choice
|
|
18
|
+
options:
|
|
19
|
+
- none
|
|
20
|
+
- testpypi
|
|
21
|
+
- pypi
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: write
|
|
25
|
+
id-token: write
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
package:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
if: |
|
|
31
|
+
github.event_name == 'workflow_dispatch' ||
|
|
32
|
+
(github.event_name == 'workflow_run' &&
|
|
33
|
+
github.event.workflow_run.conclusion == 'success' &&
|
|
34
|
+
(github.event.workflow_run.head_branch == 'main' ||
|
|
35
|
+
github.event.workflow_run.head_branch == 'master'))
|
|
36
|
+
outputs:
|
|
37
|
+
version: ${{ steps.version.outputs.version }}
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
with:
|
|
41
|
+
fetch-depth: 0
|
|
42
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
|
44
|
+
|
|
45
|
+
- name: Set up Python 3.12
|
|
46
|
+
uses: actions/setup-python@v5
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.12"
|
|
49
|
+
|
|
50
|
+
- name: Install build tools
|
|
51
|
+
run: |
|
|
52
|
+
python -m pip install --upgrade pip
|
|
53
|
+
pip install build hatchling twine
|
|
54
|
+
|
|
55
|
+
- name: Read version from pyproject.toml
|
|
56
|
+
id: version
|
|
57
|
+
run: |
|
|
58
|
+
VERSION=$(grep -E '^version = ' pyproject.toml | sed -E "s/^version = [\"'](.*)[\"']/\1/")
|
|
59
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
60
|
+
echo "Version: $VERSION"
|
|
61
|
+
|
|
62
|
+
- name: Build and check
|
|
63
|
+
run: |
|
|
64
|
+
python -m build --sdist --wheel
|
|
65
|
+
twine check dist/*
|
|
66
|
+
|
|
67
|
+
- name: Upload dist
|
|
68
|
+
uses: actions/upload-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
- name: Tag if missing
|
|
74
|
+
id: tag
|
|
75
|
+
run: |
|
|
76
|
+
VERSION="${{ steps.version.outputs.version }}"
|
|
77
|
+
git config user.name "github-actions[bot]"
|
|
78
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
79
|
+
git fetch origin --tags
|
|
80
|
+
if git rev-parse "v${VERSION}" >/dev/null 2>&1; then
|
|
81
|
+
echo "created=false" >> "$GITHUB_OUTPUT"
|
|
82
|
+
echo "Tag v${VERSION} already exists"
|
|
83
|
+
else
|
|
84
|
+
git tag -a "v${VERSION}" -m "Release v${VERSION}"
|
|
85
|
+
git push origin "v${VERSION}"
|
|
86
|
+
echo "created=true" >> "$GITHUB_OUTPUT"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
- name: GitHub Release
|
|
90
|
+
if: steps.tag.outputs.created == 'true'
|
|
91
|
+
uses: softprops/action-gh-release@v1
|
|
92
|
+
with:
|
|
93
|
+
tag_name: v${{ steps.version.outputs.version }}
|
|
94
|
+
name: Release v${{ steps.version.outputs.version }}
|
|
95
|
+
body: |
|
|
96
|
+
## Installation
|
|
97
|
+
```bash
|
|
98
|
+
pip install pyiv==${{ steps.version.outputs.version }}
|
|
99
|
+
```
|
|
100
|
+
Changelog: https://rl337.org/pyiv/changelog.html
|
|
101
|
+
files: dist/*
|
|
102
|
+
draft: false
|
|
103
|
+
prerelease: false
|
|
104
|
+
env:
|
|
105
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
106
|
+
|
|
107
|
+
publish:
|
|
108
|
+
needs: package
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish_target != 'none'
|
|
111
|
+
permissions:
|
|
112
|
+
id-token: write
|
|
113
|
+
contents: read
|
|
114
|
+
steps:
|
|
115
|
+
- name: Set up Python 3.12
|
|
116
|
+
uses: actions/setup-python@v5
|
|
117
|
+
with:
|
|
118
|
+
python-version: "3.12"
|
|
119
|
+
|
|
120
|
+
- name: Download dist
|
|
121
|
+
uses: actions/download-artifact@v4
|
|
122
|
+
with:
|
|
123
|
+
name: dist
|
|
124
|
+
path: dist
|
|
125
|
+
|
|
126
|
+
- name: Publish to TestPyPI
|
|
127
|
+
if: github.event.inputs.publish_target == 'testpypi'
|
|
128
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
129
|
+
with:
|
|
130
|
+
repository-url: https://test.pypi.org/legacy/
|
|
131
|
+
skip-existing: true
|
|
132
|
+
attestations: false
|
|
133
|
+
|
|
134
|
+
- name: Smoke-install from TestPyPI
|
|
135
|
+
if: github.event.inputs.publish_target == 'testpypi'
|
|
136
|
+
run: |
|
|
137
|
+
python -m pip install --upgrade pip
|
|
138
|
+
pip install -i https://test.pypi.org/simple/ --no-deps "pyiv==${{ needs.package.outputs.version }}"
|
|
139
|
+
python -c "import pyiv; assert pyiv.__version__ == '${{ needs.package.outputs.version }}'"
|
|
140
|
+
|
|
141
|
+
- name: Publish to PyPI
|
|
142
|
+
if: github.event.inputs.publish_target == 'pypi'
|
|
143
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|