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.
- afterpython/.pre-commit-config.yaml +48 -0
- afterpython/__init__.py +13 -0
- afterpython/_io/toml.py +103 -0
- afterpython/_io/yaml.py +26 -0
- afterpython/_paths.py +39 -0
- afterpython/_typing.py +5 -0
- afterpython/builders/__init__.py +8 -0
- afterpython/builders/metadata.py +46 -0
- afterpython/builders/molab.py +99 -0
- afterpython/cli/__init__.py +4 -0
- afterpython/cli/commands/build.py +187 -0
- afterpython/cli/commands/bump.py +88 -0
- afterpython/cli/commands/check.py +36 -0
- afterpython/cli/commands/clean.py +84 -0
- afterpython/cli/commands/commit.py +34 -0
- afterpython/cli/commands/commitizen.py +32 -0
- afterpython/cli/commands/dev.py +146 -0
- afterpython/cli/commands/format.py +28 -0
- afterpython/cli/commands/init.py +98 -0
- afterpython/cli/commands/install.py +22 -0
- afterpython/cli/commands/pre_commit.py +66 -0
- afterpython/cli/commands/preview.py +22 -0
- afterpython/cli/commands/release.py +67 -0
- afterpython/cli/commands/start.py +140 -0
- afterpython/cli/commands/sync.py +108 -0
- afterpython/cli/commands/update.py +153 -0
- afterpython/cli/main.py +71 -0
- afterpython/const.py +5 -0
- afterpython/cz.toml +27 -0
- afterpython/main.py +25 -0
- afterpython/pcu.py +164 -0
- afterpython/templates/deploy-workflow-template.yml +71 -0
- afterpython/templates/release-workflow-template.yml +56 -0
- afterpython/templates/ruff-template.toml +32 -0
- afterpython/tools/_afterpython.py +65 -0
- afterpython/tools/_git.py +111 -0
- afterpython/tools/commitizen.py +53 -0
- afterpython/tools/github_actions.py +78 -0
- afterpython/tools/myst.py +164 -0
- afterpython/tools/pre_commit.py +62 -0
- afterpython/tools/pyproject.py +72 -0
- afterpython/utils.py +215 -0
- afterpython/version.py +39 -0
- afterpython-0.1.0.dev4.dist-info/METADATA +105 -0
- afterpython-0.1.0.dev4.dist-info/RECORD +47 -0
- afterpython-0.1.0.dev4.dist-info/WHEEL +4 -0
- afterpython-0.1.0.dev4.dist-info/entry_points.txt +4 -0
- afterpython/README.py +0 -158
- afterpython/images/favicon.ico +0 -0
- afterpython/images/logo-trace.svg +0 -3
- afterpython/images/logo.png +0 -0
- afterpython/images/logo.svg +0 -2
- afterpython-0.0.1.dev2.dist-info/LICENSE +0 -201
- afterpython-0.0.1.dev2.dist-info/METADATA +0 -20
- afterpython-0.0.1.dev2.dist-info/RECORD +0 -9
- 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}")
|