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.
Files changed (113) hide show
  1. pyiv-0.3.0/.cursor/rules/packaging-docs.mdc +27 -0
  2. pyiv-0.3.0/.cursor/rules/pydoc-doctest.mdc +17 -0
  3. pyiv-0.3.0/.cursor/rules/website.mdc +19 -0
  4. pyiv-0.3.0/.cursor/skills/maintain-docs/SKILL.md +36 -0
  5. pyiv-0.3.0/.dockerignore +40 -0
  6. pyiv-0.3.0/.github/workflows/ci.yml +97 -0
  7. pyiv-0.3.0/.github/workflows/docs.yml +227 -0
  8. pyiv-0.3.0/.github/workflows/release.yml +143 -0
  9. pyiv-0.3.0/.github/workflows/version-bump.yml +158 -0
  10. pyiv-0.3.0/.gitignore +79 -0
  11. pyiv-0.3.0/.pre-commit-config.yaml +28 -0
  12. pyiv-0.3.0/AGENTS.md +279 -0
  13. pyiv-0.3.0/CHANGELOG.md +75 -0
  14. pyiv-0.3.0/Dockerfile +40 -0
  15. pyiv-0.3.0/LICENSE +26 -0
  16. pyiv-0.3.0/Makefile +46 -0
  17. pyiv-0.3.0/PKG-INFO +101 -0
  18. pyiv-0.3.0/README.md +61 -0
  19. pyiv-0.3.0/README_PRE_COMMIT.md +63 -0
  20. pyiv-0.3.0/RELEASE.md +59 -0
  21. pyiv-0.3.0/bandit-report.json +172 -0
  22. pyiv-0.3.0/check_docs_quality.py +587 -0
  23. pyiv-0.3.0/docker-compose.yml +46 -0
  24. pyiv-0.3.0/docs/README.md +31 -0
  25. pyiv-0.3.0/docs/_static/custom.css +78 -0
  26. pyiv-0.3.0/docs/changelog.rst +2 -0
  27. pyiv-0.3.0/docs/conf.py +96 -0
  28. pyiv-0.3.0/docs/guide/binding.rst +71 -0
  29. pyiv-0.3.0/docs/guide/keys.rst +105 -0
  30. pyiv-0.3.0/docs/guide/scopes.rst +62 -0
  31. pyiv-0.3.0/docs/guide/testing.rst +57 -0
  32. pyiv-0.3.0/docs/index.rst +180 -0
  33. pyiv-0.3.0/docs/modules.rst +10 -0
  34. pyiv-0.3.0/docs/pyiv/pyiv.binder.rst +13 -0
  35. pyiv-0.3.0/docs/pyiv/pyiv.binder_impl.rst +13 -0
  36. pyiv-0.3.0/docs/pyiv/pyiv.chain.rst +13 -0
  37. pyiv-0.3.0/docs/pyiv/pyiv.clock.rst +17 -0
  38. pyiv-0.3.0/docs/pyiv/pyiv.command.rst +15 -0
  39. pyiv-0.3.0/docs/pyiv/pyiv.config.rst +12 -0
  40. pyiv-0.3.0/docs/pyiv/pyiv.console.rst +16 -0
  41. pyiv-0.3.0/docs/pyiv/pyiv.datetime_service.rst +14 -0
  42. pyiv-0.3.0/docs/pyiv/pyiv.factory.rst +14 -0
  43. pyiv-0.3.0/docs/pyiv/pyiv.filesystem.rst +14 -0
  44. pyiv-0.3.0/docs/pyiv/pyiv.injector.rst +18 -0
  45. pyiv-0.3.0/docs/pyiv/pyiv.key.rst +14 -0
  46. pyiv-0.3.0/docs/pyiv/pyiv.members.rst +13 -0
  47. pyiv-0.3.0/docs/pyiv/pyiv.multibinder.rst +14 -0
  48. pyiv-0.3.0/docs/pyiv/pyiv.network.base.rst +12 -0
  49. pyiv-0.3.0/docs/pyiv/pyiv.network.clients.rst +13 -0
  50. pyiv-0.3.0/docs/pyiv/pyiv.network.rst +14 -0
  51. pyiv-0.3.0/docs/pyiv/pyiv.optional.rst +19 -0
  52. pyiv-0.3.0/docs/pyiv/pyiv.provider.rst +16 -0
  53. pyiv-0.3.0/docs/pyiv/pyiv.reflection.rst +12 -0
  54. pyiv-0.3.0/docs/pyiv/pyiv.rst +4 -0
  55. pyiv-0.3.0/docs/pyiv/pyiv.scope.rst +15 -0
  56. pyiv-0.3.0/docs/pyiv/pyiv.serde.base.rst +12 -0
  57. pyiv-0.3.0/docs/pyiv/pyiv.serde.encodings.rst +17 -0
  58. pyiv-0.3.0/docs/pyiv/pyiv.serde.json.rst +6 -0
  59. pyiv-0.3.0/docs/pyiv/pyiv.serde.rst +15 -0
  60. pyiv-0.3.0/docs/pyiv/pyiv.singleton.rst +13 -0
  61. pyiv-0.3.0/generate_index.py +353 -0
  62. pyiv-0.3.0/poetry.lock +831 -0
  63. pyiv-0.3.0/pyiv/__init__.py +137 -0
  64. pyiv-0.3.0/pyiv/binder.py +203 -0
  65. pyiv-0.3.0/pyiv/binder_impl.py +178 -0
  66. pyiv-0.3.0/pyiv/chain.py +79 -0
  67. pyiv-0.3.0/pyiv/clock.py +411 -0
  68. pyiv-0.3.0/pyiv/command.py +581 -0
  69. pyiv-0.3.0/pyiv/config.py +555 -0
  70. pyiv-0.3.0/pyiv/console.py +2143 -0
  71. pyiv-0.3.0/pyiv/datetime_service.py +115 -0
  72. pyiv-0.3.0/pyiv/factory.py +133 -0
  73. pyiv-0.3.0/pyiv/filesystem.py +801 -0
  74. pyiv-0.3.0/pyiv/injector.py +707 -0
  75. pyiv-0.3.0/pyiv/key.py +223 -0
  76. pyiv-0.3.0/pyiv/members.py +235 -0
  77. pyiv-0.3.0/pyiv/multibinder.py +291 -0
  78. pyiv-0.3.0/pyiv/network/__init__.py +20 -0
  79. pyiv-0.3.0/pyiv/network/base.py +137 -0
  80. pyiv-0.3.0/pyiv/network/clients.py +202 -0
  81. pyiv-0.3.0/pyiv/optional.py +174 -0
  82. pyiv-0.3.0/pyiv/provider.py +288 -0
  83. pyiv-0.3.0/pyiv/py.typed +0 -0
  84. pyiv-0.3.0/pyiv/reflection.py +362 -0
  85. pyiv-0.3.0/pyiv/scope.py +302 -0
  86. pyiv-0.3.0/pyiv/serde/__init__.py +47 -0
  87. pyiv-0.3.0/pyiv/serde/base.py +125 -0
  88. pyiv-0.3.0/pyiv/serde/encodings.py +373 -0
  89. pyiv-0.3.0/pyiv/serde/json.py +33 -0
  90. pyiv-0.3.0/pyiv/singleton.py +121 -0
  91. pyiv-0.3.0/pyproject.toml +97 -0
  92. pyiv-0.3.0/run_checks.sh +143 -0
  93. pyiv-0.3.0/scripts/generate_index.py +352 -0
  94. pyiv-0.3.0/scripts/style_pydoc_html.py +361 -0
  95. pyiv-0.3.0/tests/__init__.py +1 -0
  96. pyiv-0.3.0/tests/conftest.py +48 -0
  97. pyiv-0.3.0/tests/test_clock.py +163 -0
  98. pyiv-0.3.0/tests/test_command.py +710 -0
  99. pyiv-0.3.0/tests/test_console.py +630 -0
  100. pyiv-0.3.0/tests/test_datetime_service.py +148 -0
  101. pyiv-0.3.0/tests/test_factory.py +119 -0
  102. pyiv-0.3.0/tests/test_filesystem.py +129 -0
  103. pyiv-0.3.0/tests/test_injector.py +189 -0
  104. pyiv-0.3.0/tests/test_injector_factory_with_injector.py +81 -0
  105. pyiv-0.3.0/tests/test_key.py +53 -0
  106. pyiv-0.3.0/tests/test_multibinder.py +61 -0
  107. pyiv-0.3.0/tests/test_network.py +307 -0
  108. pyiv-0.3.0/tests/test_reflection.py +676 -0
  109. pyiv-0.3.0/tests/test_reflection_nested_paths.py +220 -0
  110. pyiv-0.3.0/tests/test_reflection_re_export.py +157 -0
  111. pyiv-0.3.0/tests/test_serde.py +387 -0
  112. pyiv-0.3.0/tests/test_singleton.py +293 -0
  113. 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`.
@@ -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