afterpython 0.0.1.dev2__py3-none-any.whl → 0.1.0.dev4__py3-none-any.whl

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 (56) hide show
  1. afterpython/.pre-commit-config.yaml +48 -0
  2. afterpython/__init__.py +13 -0
  3. afterpython/_io/toml.py +103 -0
  4. afterpython/_io/yaml.py +26 -0
  5. afterpython/_paths.py +39 -0
  6. afterpython/_typing.py +5 -0
  7. afterpython/builders/__init__.py +8 -0
  8. afterpython/builders/metadata.py +46 -0
  9. afterpython/builders/molab.py +99 -0
  10. afterpython/cli/__init__.py +4 -0
  11. afterpython/cli/commands/build.py +187 -0
  12. afterpython/cli/commands/bump.py +88 -0
  13. afterpython/cli/commands/check.py +36 -0
  14. afterpython/cli/commands/clean.py +84 -0
  15. afterpython/cli/commands/commit.py +34 -0
  16. afterpython/cli/commands/commitizen.py +32 -0
  17. afterpython/cli/commands/dev.py +146 -0
  18. afterpython/cli/commands/format.py +28 -0
  19. afterpython/cli/commands/init.py +98 -0
  20. afterpython/cli/commands/install.py +22 -0
  21. afterpython/cli/commands/pre_commit.py +66 -0
  22. afterpython/cli/commands/preview.py +22 -0
  23. afterpython/cli/commands/release.py +67 -0
  24. afterpython/cli/commands/start.py +140 -0
  25. afterpython/cli/commands/sync.py +108 -0
  26. afterpython/cli/commands/update.py +153 -0
  27. afterpython/cli/main.py +71 -0
  28. afterpython/const.py +5 -0
  29. afterpython/cz.toml +27 -0
  30. afterpython/main.py +25 -0
  31. afterpython/pcu.py +164 -0
  32. afterpython/templates/deploy-workflow-template.yml +71 -0
  33. afterpython/templates/release-workflow-template.yml +56 -0
  34. afterpython/templates/ruff-template.toml +32 -0
  35. afterpython/tools/_afterpython.py +65 -0
  36. afterpython/tools/_git.py +111 -0
  37. afterpython/tools/commitizen.py +53 -0
  38. afterpython/tools/github_actions.py +78 -0
  39. afterpython/tools/myst.py +164 -0
  40. afterpython/tools/pre_commit.py +62 -0
  41. afterpython/tools/pyproject.py +72 -0
  42. afterpython/utils.py +215 -0
  43. afterpython/version.py +39 -0
  44. afterpython-0.1.0.dev4.dist-info/METADATA +105 -0
  45. afterpython-0.1.0.dev4.dist-info/RECORD +47 -0
  46. afterpython-0.1.0.dev4.dist-info/WHEEL +4 -0
  47. afterpython-0.1.0.dev4.dist-info/entry_points.txt +4 -0
  48. afterpython/README.py +0 -158
  49. afterpython/images/favicon.ico +0 -0
  50. afterpython/images/logo-trace.svg +0 -3
  51. afterpython/images/logo.png +0 -0
  52. afterpython/images/logo.svg +0 -2
  53. afterpython-0.0.1.dev2.dist-info/LICENSE +0 -201
  54. afterpython-0.0.1.dev2.dist-info/METADATA +0 -20
  55. afterpython-0.0.1.dev2.dist-info/RECORD +0 -9
  56. afterpython-0.0.1.dev2.dist-info/WHEEL +0 -4
afterpython/pcu.py ADDED
@@ -0,0 +1,164 @@
1
+ """
2
+ pcu = "pip check updates", similar to ncu (npm check updates in Node.js)
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from typing import TYPE_CHECKING, TypedDict, NamedTuple, TypeAlias
7
+
8
+ if TYPE_CHECKING:
9
+ from tomlkit.toml_document import TOMLDocument
10
+
11
+ import asyncio
12
+
13
+ from packaging.requirements import Requirement
14
+
15
+ from afterpython.version import Version
16
+
17
+
18
+ class Dependency(NamedTuple):
19
+ requirement: Requirement
20
+ min_version: Version | None
21
+ max_version: Version | None = None
22
+ latest_version: Version | None = None
23
+
24
+
25
+ DependencyName: TypeAlias = str
26
+ ExtrasName: TypeAlias = str
27
+ GroupName: TypeAlias = str
28
+ FakeCategoryName: TypeAlias = str
29
+ Dependencies = TypedDict(
30
+ "Dependencies",
31
+ {
32
+ "dependencies": dict[FakeCategoryName, list[Dependency]],
33
+ "optional-dependencies": dict[ExtrasName, list[Dependency]],
34
+ "dependency-groups": dict[GroupName, list[Dependency]],
35
+ "build-system": dict[str, list[Dependency]],
36
+ },
37
+ )
38
+
39
+
40
+ def parse_min_max_versions_from_requirement(
41
+ req: Requirement,
42
+ ) -> dict[str, Version | None]:
43
+ min_ver: Version | None = None
44
+ max_ver: Version | None = None
45
+ if req.specifier:
46
+ # req.specifier is like ">=8.3.0" or ">=1.0,<2.0"
47
+ versions = sorted(Version(spec.version) for spec in req.specifier)
48
+ if len(versions) == 1:
49
+ min_ver = versions[0]
50
+ else:
51
+ min_ver, max_ver = versions[0], versions[-1]
52
+ return {
53
+ "min_version": min_ver,
54
+ "max_version": max_ver,
55
+ }
56
+
57
+
58
+ async def get_latest_versions(
59
+ requirements: list[Requirement],
60
+ ) -> dict[str, Version | None]:
61
+ """Get latest versions for a list of dependencies from PyPI."""
62
+ import httpx
63
+ from afterpython.utils import fetch_pypi_json
64
+
65
+ async def fetch_version(
66
+ client: httpx.AsyncClient, package_name: str
67
+ ) -> Version | None:
68
+ """Fetch the latest version of a package from PyPI."""
69
+ data = await fetch_pypi_json(client, package_name)
70
+ return Version(data["info"]["version"]) if data else None
71
+
72
+ async with httpx.AsyncClient(timeout=10.0) as client:
73
+ tasks = [fetch_version(client, req.name) for req in requirements]
74
+ results = await asyncio.gather(*tasks, return_exceptions=True)
75
+ return dict(zip([req.name for req in requirements], results, strict=False))
76
+
77
+
78
+ def get_dependencies() -> Dependencies:
79
+ """Get dependencies from pyproject.toml"""
80
+ from afterpython.tools.pyproject import read_pyproject
81
+
82
+ doc: TOMLDocument = read_pyproject()
83
+ dependencies = {
84
+ "dependencies": list(doc["project"].get("dependencies", [])),
85
+ "optional-dependencies": dict(doc["project"].get("optional-dependencies", {})),
86
+ "dependency-groups": dict(doc.get("dependency-groups", {})),
87
+ "build-system": dict(doc.get("build-system", {})),
88
+ }
89
+ # add "fake_category" to "dependencies" to have the same structure as "optional-dependencies" and "dependency-groups"
90
+ dependencies["dependencies"] = {"fake_category": dependencies["dependencies"]}
91
+ # only keep the "requires" key
92
+ if "requires" in dependencies["build-system"]:
93
+ dependencies["build-system"] = {
94
+ "requires": dependencies["build-system"]["requires"]
95
+ }
96
+
97
+ # convert all dependency strings to type "Requirement"
98
+ for dep_type in dependencies:
99
+ for category, deps in dependencies[dep_type].items():
100
+ dependencies[dep_type][category] = [Requirement(dep) for dep in deps]
101
+
102
+ # flatten the dependencies to a list of type "Requirement"
103
+ all_reqs = [
104
+ req
105
+ for deps_dict in dependencies.values()
106
+ for req_list in deps_dict.values()
107
+ for req in req_list
108
+ ]
109
+
110
+ # Fetch ALL latest versions in ONE async call
111
+ latest_versions = asyncio.run(get_latest_versions(all_reqs))
112
+
113
+ # convert the requirements to type "Dependency"
114
+ for dep_type in dependencies:
115
+ for category, requirements in dependencies[dep_type].items():
116
+ dependencies[dep_type][category] = [
117
+ Dependency(
118
+ **parse_min_max_versions_from_requirement(req),
119
+ requirement=req,
120
+ latest_version=latest_versions.get(req.name),
121
+ )
122
+ for req in requirements
123
+ ]
124
+
125
+ return dependencies
126
+
127
+
128
+ def update_dependencies(dependencies: Dependencies):
129
+ """Update dependency versions in pyproject.toml"""
130
+ from afterpython.tools.pyproject import read_pyproject, write_pyproject
131
+
132
+ doc: TOMLDocument = read_pyproject()
133
+ for dep_type in dependencies:
134
+ # category = extras or group name
135
+ for category, deps in dependencies[dep_type].items():
136
+ if dep_type == "dependencies":
137
+ doc_deps = doc["project"][dep_type]
138
+ elif dep_type == "optional-dependencies":
139
+ doc_deps = doc["project"][dep_type][category]
140
+ elif dep_type in ["dependency-groups", "build-system"]:
141
+ doc_deps = doc[dep_type][category]
142
+ else:
143
+ raise ValueError(f"Invalid dependency type: {dep_type}")
144
+
145
+ # Update in place
146
+ for i, (dep, package) in enumerate(zip(deps, doc_deps, strict=False)):
147
+ package: str # package = e.g. "click>=8.3.0"
148
+ req: Requirement = dep.requirement
149
+ min_ver = str(dep.min_version) if dep.min_version else None
150
+ max_ver = str(dep.max_version) if dep.max_version else None
151
+ latest_ver = str(dep.latest_version) if dep.latest_version else None
152
+ if (
153
+ req.name in package
154
+ and min_ver
155
+ and latest_ver
156
+ and min_ver in package
157
+ ):
158
+ doc_deps[i] = package.replace(min_ver, latest_ver)
159
+ if max_ver and dep.latest_version not in req.specifier:
160
+ doc_deps[i] = doc_deps[i].replace(
161
+ max_ver, str(dep.max_version.next_breaking())
162
+ )
163
+
164
+ write_pyproject(doc)
@@ -0,0 +1,71 @@
1
+ name: Deploy Project Website to GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main # Deploy when pushing to main branch
7
+ paths:
8
+ # Trigger deployment only when content or website files change
9
+ - 'afterpython/doc/**'
10
+ - 'afterpython/blog/**'
11
+ - 'afterpython/tutorial/**'
12
+ - 'afterpython/example/**'
13
+ - 'afterpython/guide/**'
14
+ - 'afterpython/static/**'
15
+ - 'afterpython/_website/**'
16
+ - 'afterpython/authors.yml'
17
+ - 'afterpython/faq.yml'
18
+ - '.github/workflows/deploy.yml'
19
+ workflow_dispatch: # Allow manual deployment from Actions tab
20
+
21
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
22
+ permissions:
23
+ contents: read
24
+ pages: write
25
+ id-token: write
26
+
27
+ # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
28
+ # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
29
+ concurrency:
30
+ group: "pages"
31
+ cancel-in-progress: false
32
+
33
+ jobs:
34
+ build:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - name: Checkout code
38
+ uses: actions/checkout@v4
39
+
40
+ - name: Set up Python
41
+ uses: actions/setup-python@v5
42
+ with:
43
+ python-version: '3.11'
44
+
45
+ - name: Install uv
46
+ uses: astral-sh/setup-uv@v5
47
+ with:
48
+ version: "latest"
49
+
50
+ - name: Install afterpython
51
+ run: uv pip install --system afterpython
52
+
53
+ - name: Build website
54
+ run: ap build
55
+
56
+ - name: Upload artifact
57
+ uses: actions/upload-pages-artifact@v3
58
+ with:
59
+ path: './afterpython/_website/build'
60
+
61
+ deploy:
62
+ environment:
63
+ name: github-pages
64
+ # This will dynamically generate the deployment URL
65
+ url: ${{ steps.deployment.outputs.page_url }}
66
+ runs-on: ubuntu-latest
67
+ needs: build
68
+ steps:
69
+ - name: Deploy to GitHub Pages
70
+ id: deployment
71
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,56 @@
1
+ name: Release to PyPI and GitHub
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*' # Trigger on all version tags (v0.1.0, v0.1.0rc1, v0.1.0.dev4, etc.)
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write # Required for creating GitHub releases
13
+ id-token: write # Required for PyPI trusted publishing
14
+
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: '3.11'
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v5
26
+ with:
27
+ version: "latest"
28
+
29
+ - name: Build package
30
+ run: uv build
31
+
32
+ - name: Publish to PyPI
33
+ run: uv publish
34
+
35
+ - name: Determine if pre-release
36
+ id: check_prerelease
37
+ run: |
38
+ # Extract version from tag (remove 'v' prefix)
39
+ VERSION="${GITHUB_REF_NAME#v}"
40
+
41
+ # Check if this is a pre-release or dev version
42
+ # Matches: dev, rc, alpha, beta, a1, b2 (but not 'stable-version')
43
+ if [[ "$VERSION" =~ (dev|rc|alpha|beta|a[0-9]+|b[0-9]+) ]]; then
44
+ echo "prerelease=true" >> $GITHUB_OUTPUT
45
+ else
46
+ echo "prerelease=false" >> $GITHUB_OUTPUT
47
+ fi
48
+
49
+ - name: Create GitHub Release
50
+ uses: softprops/action-gh-release@v2
51
+ with:
52
+ tag_name: ${{ github.ref_name }}
53
+ name: ${{ github.ref_name }}
54
+ generate_release_notes: false
55
+ prerelease: ${{ steps.check_prerelease.outputs.prerelease }}
56
+ files: dist/* # Attach built packages (wheel + sdist)
@@ -0,0 +1,32 @@
1
+ # Ruff configuration
2
+ # See More: https://docs.astral.sh/ruff/configuration/
3
+
4
+ line-length = 88
5
+ target-version = "py311"
6
+
7
+ [lint]
8
+ # ruff default rules:
9
+ select = [
10
+ "E4", # import-related errors (e.g., undefined names in imports)
11
+ "E7", # statement errors (e.g., multiple statements on one line)
12
+ "E9", # runtime errors (e.g., syntax errors, indentation issues)
13
+ "F", # Pyflakes - logical errors (e.g., unused imports, undefined variables)
14
+ ]
15
+ # ignored rules:
16
+ ignore = [
17
+ "E501", # line too long
18
+ ]
19
+ # extended rules:
20
+ extend-select = [
21
+ # "I", # isort - import sorting
22
+ "UP", # pyupgrade - upgrade syntax for newer Python
23
+ "B", # flake8-bugbear - find likely bugs
24
+ "SIM", # flake8-simplify - simplify code
25
+ # "C90", # mccabe - complexity checking
26
+ "N", # pep8-naming - enforce naming conventions
27
+ "RUF", # ruff-specific rules
28
+ ]
29
+
30
+ [format]
31
+ quote-style = "double" # double quotes for strings
32
+ indent-style = "space" # indent with spaces, rather than tabs.
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from tomlkit.toml_document import TOMLDocument
6
+
7
+ import tomlkit
8
+
9
+ import afterpython as ap
10
+ from afterpython._io.toml import read_toml, write_toml, _to_tomlkit
11
+
12
+
13
+ def read_afterpython() -> TOMLDocument:
14
+ """Read afterpython.toml"""
15
+ return read_toml(ap.paths.afterpython_path / "afterpython.toml")
16
+
17
+
18
+ def update_afterpython(data_update: dict):
19
+ """Update afterpython.toml
20
+
21
+ Args:
22
+ data_update: dict of data to update
23
+ """
24
+ from afterpython.utils import deep_merge
25
+
26
+ afterpython_toml_path = ap.paths.afterpython_path / "afterpython.toml"
27
+
28
+ # read existing data
29
+ if not afterpython_toml_path.exists():
30
+ afterpython_toml_path.touch()
31
+ existing_data = tomlkit.document()
32
+ else:
33
+ with open(afterpython_toml_path, "rb") as f:
34
+ existing_data = tomlkit.parse(f.read())
35
+ if existing_data is None:
36
+ existing_data = tomlkit.document()
37
+
38
+ # convert and update existing data
39
+ # Convert to tomlkit objects to use "array of inline tables" format
40
+ # e.g. authors = [{name = "..."}] instead of [[docs.authors]] (array of tables)
41
+ converted_data = _to_tomlkit(data_update)
42
+
43
+ existing_data = deep_merge(existing_data, converted_data)
44
+
45
+ # write updated data
46
+ write_toml(afterpython_toml_path, existing_data)
47
+
48
+
49
+ def init_afterpython():
50
+ """Initialize afterpython.toml"""
51
+ afterpython_toml_path = ap.paths.afterpython_path / "afterpython.toml"
52
+ if afterpython_toml_path.exists():
53
+ print(f"afterpython.toml already exists at {afterpython_toml_path}")
54
+ return
55
+ afterpython_toml_path.touch()
56
+ print(f"Created {afterpython_toml_path}")
57
+ update_afterpython(
58
+ {
59
+ "company": {
60
+ "name": "",
61
+ "url": "",
62
+ },
63
+ "website": {"url": ""},
64
+ }
65
+ )
@@ -0,0 +1,111 @@
1
+ # VIBE-CODED
2
+ import re
3
+ import subprocess
4
+
5
+ from git import Repo
6
+
7
+ from afterpython.utils import has_gh
8
+
9
+
10
+ def get_git_user_config() -> dict | None:
11
+ """Get Git user configuration (name and email)."""
12
+ try:
13
+ # Get the repo
14
+ repo = Repo(search_parent_directories=True)
15
+
16
+ # Access git config
17
+ reader = repo.config_reader()
18
+
19
+ # Get user name and email
20
+ name = reader.get_value("user", "name", default=None)
21
+ email = reader.get_value("user", "email", default=None)
22
+
23
+ if not name or not email:
24
+ return None
25
+
26
+ return {"name": name, "email": email}
27
+ except Exception:
28
+ return None
29
+
30
+
31
+ def get_github_url() -> str | None:
32
+ """Get GitHub repository URL from git remote origin."""
33
+ try:
34
+ # Get the repo
35
+ repo = Repo(search_parent_directories=True)
36
+
37
+ # Get origin remote URL
38
+ if "origin" not in repo.remotes:
39
+ return None
40
+
41
+ remote_url = repo.remotes.origin.url
42
+ # Verify it's a GitHub URL
43
+ if "github.com" not in remote_url:
44
+ return None
45
+
46
+ # Convert SSH format to HTTPS format
47
+ # git@github.com:user/repo.git -> https://github.com/user/repo
48
+ if remote_url.startswith("git@github.com:"):
49
+ remote_url = remote_url.replace("git@github.com:", "https://github.com/")
50
+
51
+ # Remove .git suffix if present
52
+ remote_url = re.sub(r"\.git$", "", remote_url)
53
+
54
+ return remote_url
55
+
56
+ except (ImportError, Exception):
57
+ # GitPython not installed or not in a git repo
58
+ return None
59
+
60
+
61
+ def setup_github_auth():
62
+ """Guide user through GitHub authentication."""
63
+ if not has_gh():
64
+ print("""
65
+ ╭─────────────────────────────────────────╮
66
+ │ GitHub CLI Required │
67
+ ╰─────────────────────────────────────────╯
68
+
69
+ Install it:
70
+ • macOS: brew install gh
71
+ • Linux: https://github.com/cli/cli/releases
72
+ • Windows: https://cli.github.com/
73
+
74
+ """)
75
+ return False
76
+
77
+ result = subprocess.run(["gh", "auth", "status"], capture_output=True, check=False)
78
+
79
+ if result.returncode != 0:
80
+ print("""
81
+ ╭─────────────────────────────────────────╮
82
+ │ GitHub Authentication Required │
83
+ ╰─────────────────────────────────────────╯
84
+
85
+ Please authenticate with GitHub:
86
+
87
+ gh auth login
88
+
89
+ """)
90
+ return False
91
+
92
+ print("āœ“ GitHub authentication verified")
93
+ return True
94
+
95
+
96
+ def get_github_username() -> str | None:
97
+ """Get authenticated username via gh CLI."""
98
+ if not has_gh():
99
+ return None
100
+
101
+ result = subprocess.run(
102
+ ["gh", "api", "user", "--jq", ".login"],
103
+ capture_output=True,
104
+ text=True,
105
+ check=True,
106
+ )
107
+ return result.stdout.strip()
108
+
109
+
110
+ # TODO: use gh's token for pygithub to get repo issues
111
+ # def get_repo_issues(owner: str, repo: str) -> list[dict]:
@@ -0,0 +1,53 @@
1
+ import afterpython as ap
2
+
3
+
4
+ commitizen_default = {
5
+ "tool": {
6
+ "commitizen": {
7
+ "name": "cz_conventional_commits",
8
+ "tag_format": "v$version",
9
+ "version_scheme": "pep440",
10
+ "version_provider": "uv",
11
+ "update_changelog_on_bump": False,
12
+ "major_version_zero": True,
13
+ }
14
+ }
15
+ }
16
+
17
+
18
+ commitizen_pre_commit_hook = {
19
+ "repos": [
20
+ {
21
+ "repo": "https://github.com/commitizen-tools/commitizen",
22
+ "rev": "v4.10.0",
23
+ "hooks": [
24
+ {
25
+ "id": "commitizen",
26
+ "stages": ["commit-msg"],
27
+ },
28
+ {
29
+ "id": "commitizen-branch",
30
+ "stages": ["pre-push"],
31
+ },
32
+ ],
33
+ },
34
+ ]
35
+ }
36
+
37
+
38
+ def init_commitizen():
39
+ from afterpython.tools.pre_commit import update_pre_commit
40
+ from afterpython._io.toml import write_toml
41
+
42
+ afterpython_path = ap.paths.afterpython_path
43
+ commitizen_toml_path = afterpython_path / "cz.toml"
44
+ if commitizen_toml_path.exists():
45
+ print(f"Commitizen configuration file {commitizen_toml_path} already exists")
46
+ return
47
+
48
+ # Create commitizen configuration with default values
49
+ write_toml(commitizen_toml_path, commitizen_default)
50
+ print(f"Created {commitizen_toml_path}")
51
+
52
+ # add commitizen hook to .pre-commit-config.yaml
53
+ update_pre_commit(commitizen_pre_commit_hook)
@@ -0,0 +1,78 @@
1
+ import shutil
2
+
3
+ import click
4
+
5
+ import afterpython as ap
6
+
7
+
8
+ def init_release_workflow():
9
+ """Initialize GitHub Actions release workflow.
10
+
11
+ Creates .github/workflows/release.yml in the user's project root.
12
+ This workflow triggers on version tags and:
13
+ - Builds the package with uv
14
+ - Publishes to PyPI using trusted publishing
15
+ - Creates GitHub releases (with pre-release detection)
16
+ """
17
+ user_path = ap.paths.user_path
18
+ workflow_dir = user_path / ".github" / "workflows"
19
+ workflow_path = workflow_dir / "release.yml"
20
+
21
+ if workflow_path.exists():
22
+ click.echo(f"GitHub Actions release workflow {workflow_path} already exists")
23
+ return
24
+
25
+ # Create .github/workflows directory if it doesn't exist
26
+ workflow_dir.mkdir(parents=True, exist_ok=True)
27
+
28
+ # Copy template from package
29
+ template_path = ap.paths.templates_path / "release-workflow-template.yml"
30
+ if not template_path.exists():
31
+ raise FileNotFoundError(
32
+ f"Template file not found: {template_path}\n"
33
+ "This might indicate a corrupted installation. Please reinstall afterpython."
34
+ )
35
+
36
+ shutil.copy(template_path, workflow_path)
37
+ click.echo(f"Created {workflow_path}")
38
+ click.echo(
39
+ "\nšŸ“‹ Next steps to enable releases:\n"
40
+ " 1. Enable PyPI trusted publishing:\n"
41
+ " - Go to https://pypi.org/manage/account/publishing/\n"
42
+ " - Add your repository and workflow file: release.yml\n"
43
+ " 2. Push tags to trigger releases:\n"
44
+ " - Use 'ap bump --release' for stable releases (auto-pushes tag)\n"
45
+ " - Use 'ap bump --pre' then 'ap release' for pre-releases\n"
46
+ )
47
+
48
+
49
+ def init_deploy_workflow():
50
+ """Initialize GitHub Actions deployment workflow for GitHub Pages.
51
+
52
+ Creates .github/workflows/deploy.yml in the user's project root.
53
+ This workflow triggers on pushes to main branch and:
54
+ - Builds the project website with 'ap build'
55
+ - Deploys to GitHub Pages
56
+ - Allows manual deployment via workflow_dispatch
57
+ """
58
+ user_path = ap.paths.user_path
59
+ workflow_dir = user_path / ".github" / "workflows"
60
+ workflow_path = workflow_dir / "deploy.yml"
61
+
62
+ if workflow_path.exists():
63
+ click.echo(f"GitHub Actions deploy workflow {workflow_path} already exists")
64
+ return
65
+
66
+ # Create .github/workflows directory if it doesn't exist
67
+ workflow_dir.mkdir(parents=True, exist_ok=True)
68
+
69
+ # Copy template from package
70
+ template_path = ap.paths.templates_path / "deploy-workflow-template.yml"
71
+ if not template_path.exists():
72
+ raise FileNotFoundError(
73
+ f"Template file not found: {template_path}\n"
74
+ "This might indicate a corrupted installation. Please reinstall afterpython."
75
+ )
76
+
77
+ shutil.copy(template_path, workflow_path)
78
+ click.echo(f"Created {workflow_path}")