afterpython 0.0.1.dev2__py3-none-any.whl → 0.1.0__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/__init__.py +13 -0
- afterpython/_io/toml.py +105 -0
- afterpython/_io/yaml.py +26 -0
- afterpython/_paths.py +39 -0
- afterpython/_typing.py +4 -0
- afterpython/builders/__init__.py +17 -0
- afterpython/builders/content_json.py +131 -0
- afterpython/builders/index_md.py +94 -0
- afterpython/builders/jupyter_notebook.py +293 -0
- afterpython/builders/markdown.py +271 -0
- afterpython/builders/metadata.py +50 -0
- afterpython/cli/__init__.py +3 -0
- afterpython/cli/commands/add.py +67 -0
- afterpython/cli/commands/build.py +260 -0
- afterpython/cli/commands/bump.py +118 -0
- afterpython/cli/commands/check.py +36 -0
- afterpython/cli/commands/clean.py +85 -0
- afterpython/cli/commands/commit.py +52 -0
- afterpython/cli/commands/commitizen.py +32 -0
- afterpython/cli/commands/dev.py +213 -0
- afterpython/cli/commands/format.py +28 -0
- afterpython/cli/commands/init.py +107 -0
- afterpython/cli/commands/init_branch_rules.py +9 -0
- afterpython/cli/commands/install.py +28 -0
- afterpython/cli/commands/lock.py +23 -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/remove.py +67 -0
- afterpython/cli/commands/start.py +144 -0
- afterpython/cli/commands/sync.py +152 -0
- afterpython/cli/commands/update.py +176 -0
- afterpython/cli/main.py +132 -0
- afterpython/const.py +4 -0
- afterpython/main.py +25 -0
- afterpython/pcu.py +166 -0
- afterpython/templates/ci-workflow-template.yml +197 -0
- afterpython/templates/cz-template.toml +7 -0
- afterpython/templates/dependabot-template.yml +30 -0
- afterpython/templates/deploy-workflow-template.yml +88 -0
- afterpython/templates/pre-commit-config-template.yaml +48 -0
- afterpython/templates/release-workflow-template.yml +56 -0
- afterpython/templates/ruff-template.toml +32 -0
- afterpython/tools/_afterpython.py +128 -0
- afterpython/tools/_git.py +137 -0
- afterpython/tools/branch_rules.py +106 -0
- afterpython/tools/commitizen.py +13 -0
- afterpython/tools/github_actions.py +48 -0
- afterpython/tools/myst.py +208 -0
- afterpython/tools/pre_commit.py +37 -0
- afterpython/tools/pyproject.py +110 -0
- afterpython/utils.py +299 -0
- afterpython/version.py +39 -0
- afterpython-0.1.0.dist-info/METADATA +131 -0
- afterpython-0.1.0.dist-info/RECORD +57 -0
- afterpython-0.1.0.dist-info/WHEEL +4 -0
- afterpython-0.1.0.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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from afterpython._typing import NodeEnv
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from click.exceptions import Exit
|
|
12
|
+
|
|
13
|
+
command_kwargs = {
|
|
14
|
+
"add_help_option": False, # disable click's --help option so that myst start --help can work
|
|
15
|
+
"context_settings": dict(
|
|
16
|
+
ignore_unknown_options=True,
|
|
17
|
+
allow_extra_args=True,
|
|
18
|
+
),
|
|
19
|
+
"hidden": True, # Hide these commands from help output
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command(**command_kwargs)
|
|
24
|
+
@click.pass_context
|
|
25
|
+
@click.option(
|
|
26
|
+
"--doc", is_flag=True, help="Start the development server for doc/ directory"
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--blog", is_flag=True, help="Start the development server for blog/ directory"
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--tutorial",
|
|
33
|
+
is_flag=True,
|
|
34
|
+
help="Start the development server for tutorial/ directory",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--example",
|
|
38
|
+
is_flag=True,
|
|
39
|
+
help="Start the development server for example/ directory",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--guide", is_flag=True, help="Start the development server for guide/ directory"
|
|
43
|
+
)
|
|
44
|
+
def start(ctx, doc: bool, blog: bool, tutorial: bool, example: bool, guide: bool):
|
|
45
|
+
"""Start MyST development server for a specific content type"""
|
|
46
|
+
from afterpython.const import CONTENT_TYPES
|
|
47
|
+
from afterpython.utils import (
|
|
48
|
+
find_node_env,
|
|
49
|
+
handle_passthrough_help,
|
|
50
|
+
has_content_for_myst,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Show both our options and myst's help and exit
|
|
54
|
+
handle_passthrough_help(
|
|
55
|
+
ctx,
|
|
56
|
+
["myst", "start"],
|
|
57
|
+
show_underlying=True,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
node_env: NodeEnv = find_node_env()
|
|
61
|
+
paths = ctx.obj["paths"]
|
|
62
|
+
|
|
63
|
+
if doc:
|
|
64
|
+
content_type = "doc"
|
|
65
|
+
elif blog:
|
|
66
|
+
content_type = "blog"
|
|
67
|
+
elif tutorial:
|
|
68
|
+
content_type = "tutorial"
|
|
69
|
+
elif example:
|
|
70
|
+
content_type = "example"
|
|
71
|
+
elif guide:
|
|
72
|
+
content_type = "guide"
|
|
73
|
+
else:
|
|
74
|
+
raise click.ClickException("No content type specified")
|
|
75
|
+
|
|
76
|
+
assert content_type in CONTENT_TYPES, f"Invalid content type: {content_type}"
|
|
77
|
+
|
|
78
|
+
# assert only one of the options is True
|
|
79
|
+
if sum([doc, blog, tutorial, example, guide]) != 1:
|
|
80
|
+
raise click.ClickException("Only one content type can be specified")
|
|
81
|
+
|
|
82
|
+
content_path = paths.afterpython_path / content_type
|
|
83
|
+
|
|
84
|
+
# Check if directory has any content files
|
|
85
|
+
if not has_content_for_myst(content_path):
|
|
86
|
+
click.echo(f"Skipping {content_type}/ (no content files found)")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
result = subprocess.run(
|
|
90
|
+
["myst", "start", *ctx.args], cwd=content_path, env=node_env, check=False
|
|
91
|
+
)
|
|
92
|
+
if result.returncode != 0:
|
|
93
|
+
raise Exit(result.returncode)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run(ctx):
|
|
97
|
+
# Get the name of the function that called _run
|
|
98
|
+
import inspect
|
|
99
|
+
|
|
100
|
+
caller_frame = inspect.currentframe().f_back
|
|
101
|
+
command_name = (
|
|
102
|
+
caller_frame.f_code.co_name
|
|
103
|
+
) # e.g. doc, blog, tutorial, example, guide
|
|
104
|
+
|
|
105
|
+
# Create a new context for start that includes extra args for `myst start`
|
|
106
|
+
with start.make_context(
|
|
107
|
+
"start", [f"--{command_name}", *ctx.args], parent=ctx.parent
|
|
108
|
+
) as start_ctx:
|
|
109
|
+
return start.invoke(start_ctx)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@click.command(**command_kwargs)
|
|
113
|
+
@click.pass_context
|
|
114
|
+
def doc(ctx):
|
|
115
|
+
"""Start the development server for doc/ directory (equivalent to: start --doc)"""
|
|
116
|
+
_run(ctx)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@click.command(**command_kwargs)
|
|
120
|
+
@click.pass_context
|
|
121
|
+
def blog(ctx):
|
|
122
|
+
"""Start the development server for blog/ directory (equivalent to: start --blog)"""
|
|
123
|
+
_run(ctx)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@click.command(**command_kwargs)
|
|
127
|
+
@click.pass_context
|
|
128
|
+
def tutorial(ctx):
|
|
129
|
+
"""Start the development server for tutorial/ directory (equivalent to: start --tutorial)"""
|
|
130
|
+
_run(ctx)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@click.command(**command_kwargs)
|
|
134
|
+
@click.pass_context
|
|
135
|
+
def example(ctx):
|
|
136
|
+
"""Start the development server for example/ directory (equivalent to: start --example)"""
|
|
137
|
+
_run(ctx)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@click.command(**command_kwargs)
|
|
141
|
+
@click.pass_context
|
|
142
|
+
def guide(ctx):
|
|
143
|
+
"""Start the development server for guide/ directory (equivalent to: start --guide)"""
|
|
144
|
+
_run(ctx)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pyproject_metadata import StandardMetadata
|
|
5
|
+
from tomlkit.toml_document import TOMLDocument
|
|
6
|
+
|
|
7
|
+
import afterpython as ap
|
|
8
|
+
from afterpython.utils import convert_author_name_to_id, normalize_static_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _sync_authors_yml(authors: list[tuple[str, str | None]]):
|
|
12
|
+
"""Sync authors.yml with authors in pyproject.toml"""
|
|
13
|
+
from afterpython._io.yaml import read_yaml
|
|
14
|
+
from afterpython.tools.myst import update_authors_yml
|
|
15
|
+
|
|
16
|
+
# read myst.yml from docs path to get "version"
|
|
17
|
+
doc_myst_yml = read_yaml(ap.paths.afterpython_path / "doc" / "myst.yml")
|
|
18
|
+
data = {
|
|
19
|
+
"version": doc_myst_yml["version"],
|
|
20
|
+
"project": {
|
|
21
|
+
"contributors": [
|
|
22
|
+
# NOTE: author is a tuple of (name, email), so author[0] is the name and author[1] is the email
|
|
23
|
+
{
|
|
24
|
+
"id": convert_author_name_to_id(str(author[0])),
|
|
25
|
+
"name": str(author[0]),
|
|
26
|
+
"email": str(author[1]),
|
|
27
|
+
# "github": ...
|
|
28
|
+
}
|
|
29
|
+
for author in authors
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
update_authors_yml(data)
|
|
34
|
+
click.echo("✓ Synced authors.yml with pyproject.toml")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.command()
|
|
38
|
+
def sync():
|
|
39
|
+
"""Sync configuration between pyproject.toml/afterpython.toml and authors.yml/myst.yml"""
|
|
40
|
+
from afterpython._io.toml import _from_tomlkit
|
|
41
|
+
from afterpython.const import CONTENT_TYPES
|
|
42
|
+
from afterpython.tools._afterpython import read_afterpython
|
|
43
|
+
from afterpython.tools.myst import update_myst_yml
|
|
44
|
+
from afterpython.tools.pyproject import (
|
|
45
|
+
read_metadata,
|
|
46
|
+
read_pyproject,
|
|
47
|
+
write_pyproject,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
pyproject: StandardMetadata = read_metadata()
|
|
51
|
+
project_name = str(pyproject.name)
|
|
52
|
+
afterpython = read_afterpython()
|
|
53
|
+
github_url = str(pyproject.urls.get("repository", ""))
|
|
54
|
+
company_name = str(_from_tomlkit(afterpython.get("company", {})).get("name", ""))
|
|
55
|
+
company_url = str(_from_tomlkit(afterpython.get("company", {})).get("url", ""))
|
|
56
|
+
website_url = str(_from_tomlkit(afterpython.get("website", {})).get("url", ""))
|
|
57
|
+
try:
|
|
58
|
+
website_favicon = normalize_static_path(
|
|
59
|
+
str(_from_tomlkit(afterpython.get("website", {})).get("favicon", ""))
|
|
60
|
+
)
|
|
61
|
+
website_logo = normalize_static_path(
|
|
62
|
+
str(_from_tomlkit(afterpython.get("website", {})).get("logo", ""))
|
|
63
|
+
)
|
|
64
|
+
website_logo_dark = normalize_static_path(
|
|
65
|
+
str(_from_tomlkit(afterpython.get("website", {})).get("logo_dark", ""))
|
|
66
|
+
)
|
|
67
|
+
website_thumbnail = normalize_static_path(
|
|
68
|
+
str(_from_tomlkit(afterpython.get("website", {})).get("thumbnail", ""))
|
|
69
|
+
)
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
click.echo(f"Error in reading afterpython.toml: {e}")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
authors = pyproject.authors
|
|
75
|
+
if company_name and company_url:
|
|
76
|
+
nav_bar = [{"title": company_name, "url": company_url}]
|
|
77
|
+
else:
|
|
78
|
+
nav_bar = []
|
|
79
|
+
if website_url:
|
|
80
|
+
nav_bar.extend(
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
"title": content_type.capitalize() + "s",
|
|
84
|
+
"url": f"{website_url}/{content_type}",
|
|
85
|
+
}
|
|
86
|
+
for content_type in CONTENT_TYPES
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
_sync_authors_yml(authors)
|
|
91
|
+
|
|
92
|
+
# update "homepage", "repository", and "documentation" in pyproject.toml
|
|
93
|
+
if website_url:
|
|
94
|
+
doc: TOMLDocument = read_pyproject()
|
|
95
|
+
doc["project"]["urls"]["homepage"] = website_url
|
|
96
|
+
doc["project"]["urls"]["documentation"] = f"{website_url}/doc"
|
|
97
|
+
write_pyproject(doc)
|
|
98
|
+
|
|
99
|
+
# update myst.yml files for each content type (e.g. doc/, blog/, tutorial/, example/, guide/)
|
|
100
|
+
# based on the current values in pyproject.toml and afterpython.toml
|
|
101
|
+
for content_type in CONTENT_TYPES:
|
|
102
|
+
path = ap.paths.afterpython_path / content_type
|
|
103
|
+
# nav_bar_per_content_type = [
|
|
104
|
+
# item for item in nav_bar if item["title"] != content_type.capitalize() + "s"
|
|
105
|
+
# ]
|
|
106
|
+
title = project_name + f"'s {content_type.capitalize()}"
|
|
107
|
+
data = {
|
|
108
|
+
"project": {
|
|
109
|
+
# using author ids defined in authors.yml
|
|
110
|
+
"authors": [
|
|
111
|
+
convert_author_name_to_id(str(author[0])) for author in authors
|
|
112
|
+
],
|
|
113
|
+
"venue": {
|
|
114
|
+
# NOTE: company's name is used as the venue title
|
|
115
|
+
"title": company_name,
|
|
116
|
+
"url": company_url,
|
|
117
|
+
},
|
|
118
|
+
"copyright": f"© {company_name or project_name} {datetime.now().year}. All rights reserved.",
|
|
119
|
+
"title": "",
|
|
120
|
+
"description": str(pyproject.description),
|
|
121
|
+
"keywords": list(map(str, pyproject.keywords)),
|
|
122
|
+
"github": github_url,
|
|
123
|
+
"thumbnail": "../static" + website_thumbnail
|
|
124
|
+
if website_thumbnail
|
|
125
|
+
else "",
|
|
126
|
+
},
|
|
127
|
+
"site": {
|
|
128
|
+
"title": title,
|
|
129
|
+
"options": {
|
|
130
|
+
"favicon": "../static" + website_favicon if website_favicon else "",
|
|
131
|
+
"logo": "../static" + website_logo if website_logo else "",
|
|
132
|
+
"logo_dark": "../static" + website_logo_dark
|
|
133
|
+
if website_logo_dark
|
|
134
|
+
else "",
|
|
135
|
+
"logo_text": project_name,
|
|
136
|
+
"logo_url": website_url,
|
|
137
|
+
},
|
|
138
|
+
# FIXME: disable nav bar for now until myst fixes the issue with the nav bar
|
|
139
|
+
# the current issue is they prepend the BASE_URL to the nav bar links even they are external
|
|
140
|
+
# "nav": nav_bar_per_content_type,
|
|
141
|
+
"actions": [
|
|
142
|
+
{
|
|
143
|
+
"title": "⭐ Star",
|
|
144
|
+
"url": github_url,
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
update_myst_yml(data, path)
|
|
150
|
+
click.echo(
|
|
151
|
+
f"✓ Synced myst.yml in {path.name}/ with pyproject.toml and afterpython.toml"
|
|
152
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from afterpython._typing import NodeEnv
|
|
7
|
+
from afterpython.pcu import Dependencies
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from click.exceptions import Exit
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def update():
|
|
18
|
+
"""Update website template, pyproject.toml dependencies, etc."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@update.command()
|
|
23
|
+
@click.option(
|
|
24
|
+
"-u",
|
|
25
|
+
"--upgrade",
|
|
26
|
+
is_flag=True,
|
|
27
|
+
help="Upgrade dependencies to their latest versions",
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--all",
|
|
31
|
+
is_flag=True,
|
|
32
|
+
help="Also update pre-commit hooks and pixi dependencies",
|
|
33
|
+
)
|
|
34
|
+
def dependencies(upgrade: bool, all: bool):
|
|
35
|
+
"""Check and update project dependencies to latest versions"""
|
|
36
|
+
from afterpython.pcu import get_dependencies, update_dependencies
|
|
37
|
+
from afterpython.utils import has_pixi, has_uv
|
|
38
|
+
|
|
39
|
+
dependencies: Dependencies = get_dependencies()
|
|
40
|
+
has_at_least_one_update = False
|
|
41
|
+
for dep_type in dependencies:
|
|
42
|
+
if not len(dependencies[dep_type]):
|
|
43
|
+
continue
|
|
44
|
+
click.echo(f"- {dep_type} package(s):")
|
|
45
|
+
# category = extras or group name
|
|
46
|
+
for category, deps in dependencies[dep_type].items():
|
|
47
|
+
if dep_type in ["dependencies", "build-system"]:
|
|
48
|
+
category_name = ""
|
|
49
|
+
elif dep_type == "optional-dependencies":
|
|
50
|
+
category_name = f"extras: {category}"
|
|
51
|
+
elif dep_type == "dependency-groups":
|
|
52
|
+
category_name = f"group: {category}"
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError(f"Invalid dependency type: {dep_type}")
|
|
55
|
+
for dep in deps:
|
|
56
|
+
msg = f" {dep.requirement.name}: {dep.min_version}"
|
|
57
|
+
has_update = dep.min_version != dep.latest_version
|
|
58
|
+
if has_update:
|
|
59
|
+
has_at_least_one_update = True
|
|
60
|
+
msg += (
|
|
61
|
+
f" → {click.style(dep.latest_version, fg='green', bold=True)}"
|
|
62
|
+
)
|
|
63
|
+
if category_name:
|
|
64
|
+
msg += f" ({category_name})"
|
|
65
|
+
click.echo(msg)
|
|
66
|
+
if not has_at_least_one_update:
|
|
67
|
+
click.echo(f"\n{click.style('No dependencies to update.', bold=True)}")
|
|
68
|
+
return
|
|
69
|
+
if has_at_least_one_update and upgrade:
|
|
70
|
+
update_dependencies(dependencies) # write the latest versions to pyproject.toml
|
|
71
|
+
if has_uv():
|
|
72
|
+
click.echo("Upgrading dependencies with uv...")
|
|
73
|
+
result = subprocess.run(["uv", "lock"], check=False)
|
|
74
|
+
if result.returncode != 0:
|
|
75
|
+
raise Exit(result.returncode)
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["uv", "sync", "--all-extras", "--all-groups"], check=False
|
|
78
|
+
)
|
|
79
|
+
if result.returncode != 0:
|
|
80
|
+
raise Exit(result.returncode)
|
|
81
|
+
click.echo(
|
|
82
|
+
click.style(
|
|
83
|
+
"✓ All dependencies in pyproject.toml upgraded successfully 🎉",
|
|
84
|
+
fg="green",
|
|
85
|
+
bold=True,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
click.echo(
|
|
90
|
+
"uv not found. Updated pyproject.toml only (packages not installed)."
|
|
91
|
+
)
|
|
92
|
+
if all:
|
|
93
|
+
subprocess.run(["ap", "pre-commit", "autoupdate"])
|
|
94
|
+
click.echo("All pre-commit hooks updated successfully.")
|
|
95
|
+
if has_pixi():
|
|
96
|
+
click.echo("Upgrading dependencies with pixi...")
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
["pixi", "upgrade", "--exclude", "python"], check=False
|
|
99
|
+
)
|
|
100
|
+
if result.returncode != 0:
|
|
101
|
+
raise Exit(result.returncode)
|
|
102
|
+
result = subprocess.run(["pixi", "lock"], check=False)
|
|
103
|
+
if result.returncode != 0:
|
|
104
|
+
raise Exit(result.returncode)
|
|
105
|
+
result = subprocess.run(["pixi", "install"], check=False)
|
|
106
|
+
if result.returncode != 0:
|
|
107
|
+
raise Exit(result.returncode)
|
|
108
|
+
click.echo(
|
|
109
|
+
click.style(
|
|
110
|
+
"✓ All dependencies in pixi.toml upgraded successfully 🎉",
|
|
111
|
+
fg="green",
|
|
112
|
+
bold=True,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
update.add_command(dependencies, name="deps") # alias for "dependencies"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@update.command()
|
|
121
|
+
@click.pass_context
|
|
122
|
+
@click.option(
|
|
123
|
+
"--no-backup",
|
|
124
|
+
is_flag=True,
|
|
125
|
+
help="Skip creating a backup of the existing website template",
|
|
126
|
+
)
|
|
127
|
+
def website(ctx, no_backup: bool):
|
|
128
|
+
"""Update project website template to the latest version"""
|
|
129
|
+
from afterpython.utils import find_node_env
|
|
130
|
+
|
|
131
|
+
website_template_repo = "AfterPythonOrg/project-website-template"
|
|
132
|
+
|
|
133
|
+
paths = ctx.obj["paths"]
|
|
134
|
+
website_path = paths.website_path
|
|
135
|
+
if not no_backup:
|
|
136
|
+
backup_path = website_path.parent / "_website.backup"
|
|
137
|
+
if backup_path.exists():
|
|
138
|
+
click.echo(f"Removing old backup at {backup_path}...")
|
|
139
|
+
shutil.rmtree(backup_path)
|
|
140
|
+
if website_path.exists():
|
|
141
|
+
click.echo(f"Creating backup at {backup_path}...")
|
|
142
|
+
shutil.copytree(
|
|
143
|
+
website_path,
|
|
144
|
+
backup_path,
|
|
145
|
+
ignore=shutil.ignore_patterns("node_modules", ".svelte-kit"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Remove old template (but keep node_modules for faster reinstall)
|
|
149
|
+
if website_path.exists():
|
|
150
|
+
click.echo("Removing old project website template...")
|
|
151
|
+
shutil.rmtree(website_path)
|
|
152
|
+
website_path.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
click.echo("Updating the project website template...")
|
|
156
|
+
node_env: NodeEnv = find_node_env()
|
|
157
|
+
result = subprocess.run(
|
|
158
|
+
["pnpx", "degit", website_template_repo, str(website_path)],
|
|
159
|
+
env=node_env,
|
|
160
|
+
check=False,
|
|
161
|
+
)
|
|
162
|
+
if result.returncode != 0:
|
|
163
|
+
raise Exit(result.returncode)
|
|
164
|
+
result = subprocess.run(
|
|
165
|
+
["pnpm", "install"], cwd=website_path, env=node_env, check=False
|
|
166
|
+
)
|
|
167
|
+
if result.returncode != 0:
|
|
168
|
+
raise Exit(result.returncode)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
click.echo(f"✗ Error updating project website template: {e}", err=True)
|
|
171
|
+
if not no_backup:
|
|
172
|
+
click.echo("Restoring from backup...")
|
|
173
|
+
if website_path.exists():
|
|
174
|
+
shutil.rmtree(website_path)
|
|
175
|
+
shutil.copytree(backup_path, website_path)
|
|
176
|
+
raise
|
afterpython/cli/main.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from dotenv import find_dotenv, load_dotenv
|
|
7
|
+
from trogon import tui
|
|
8
|
+
|
|
9
|
+
import afterpython as ap
|
|
10
|
+
from afterpython import __version__
|
|
11
|
+
from afterpython.cli.commands.add import add
|
|
12
|
+
from afterpython.cli.commands.build import build
|
|
13
|
+
from afterpython.cli.commands.bump import bump
|
|
14
|
+
from afterpython.cli.commands.check import check
|
|
15
|
+
from afterpython.cli.commands.clean import clean
|
|
16
|
+
from afterpython.cli.commands.commit import commit
|
|
17
|
+
from afterpython.cli.commands.commitizen import commitizen
|
|
18
|
+
from afterpython.cli.commands.dev import dev
|
|
19
|
+
from afterpython.cli.commands.format import format
|
|
20
|
+
from afterpython.cli.commands.init import init
|
|
21
|
+
from afterpython.cli.commands.init_branch_rules import init_branch_rules
|
|
22
|
+
from afterpython.cli.commands.install import install
|
|
23
|
+
from afterpython.cli.commands.lock import lock
|
|
24
|
+
from afterpython.cli.commands.pre_commit import pre_commit
|
|
25
|
+
from afterpython.cli.commands.preview import preview
|
|
26
|
+
from afterpython.cli.commands.release import release
|
|
27
|
+
from afterpython.cli.commands.remove import remove
|
|
28
|
+
from afterpython.cli.commands.start import blog, doc, example, guide, start, tutorial
|
|
29
|
+
from afterpython.cli.commands.sync import sync
|
|
30
|
+
from afterpython.cli.commands.update import update
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AliasGroup(click.Group):
|
|
34
|
+
"""Custom group that displays command aliases together."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *args, **kwargs):
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self._aliases = defaultdict(list) # Maps primary command -> list of aliases
|
|
39
|
+
self._alias_to_primary = {} # Maps alias -> primary command name
|
|
40
|
+
|
|
41
|
+
def add_command(self, cmd, name=None):
|
|
42
|
+
"""Track aliases when the same command is added with different names."""
|
|
43
|
+
name = name or cmd.name
|
|
44
|
+
|
|
45
|
+
# Check if this command object was already added under a different name
|
|
46
|
+
for existing_name, existing_cmd in self.commands.items():
|
|
47
|
+
if existing_cmd is cmd and existing_name != name:
|
|
48
|
+
# This is an alias
|
|
49
|
+
self._aliases[existing_name].append(name)
|
|
50
|
+
self._alias_to_primary[name] = existing_name
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
super().add_command(cmd, name)
|
|
54
|
+
|
|
55
|
+
def format_commands(self, ctx, formatter):
|
|
56
|
+
"""Format commands with aliases shown together."""
|
|
57
|
+
commands = []
|
|
58
|
+
for subcommand in self.list_commands(ctx):
|
|
59
|
+
# Skip aliases - they'll be shown with their primary command
|
|
60
|
+
if subcommand in self._alias_to_primary:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
cmd = self.get_command(ctx, subcommand)
|
|
64
|
+
if cmd is None:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Skip hidden commands
|
|
68
|
+
if cmd.hidden:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
help_text = cmd.get_short_help_str(limit=formatter.width)
|
|
72
|
+
|
|
73
|
+
# Build command name with aliases
|
|
74
|
+
aliases = self._aliases.get(subcommand, [])
|
|
75
|
+
if aliases:
|
|
76
|
+
subcommand = f"{subcommand}, {', '.join(aliases)}"
|
|
77
|
+
|
|
78
|
+
commands.append((subcommand, help_text))
|
|
79
|
+
|
|
80
|
+
if commands:
|
|
81
|
+
with formatter.section("Commands"):
|
|
82
|
+
formatter.write_dl(commands)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@tui(command="tui", help="Open terminal UI")
|
|
86
|
+
@click.group(cls=AliasGroup, context_settings={"help_option_names": ["-h", "--help"]})
|
|
87
|
+
@click.pass_context
|
|
88
|
+
@click.version_option(version=__version__)
|
|
89
|
+
def afterpython_group(ctx):
|
|
90
|
+
"""afterpython's CLI"""
|
|
91
|
+
load_dotenv(find_dotenv()) # Load environment variables from .env file
|
|
92
|
+
ctx.ensure_object(dict)
|
|
93
|
+
ctx.obj["paths"] = ap.paths
|
|
94
|
+
|
|
95
|
+
# Auto-sync before commands (except sync itself to avoid recursion)
|
|
96
|
+
if (
|
|
97
|
+
ctx.invoked_subcommand
|
|
98
|
+
and ctx.invoked_subcommand not in ["sync", "init"]
|
|
99
|
+
and os.getenv("AP_AUTO_SYNC", "0") == "1"
|
|
100
|
+
):
|
|
101
|
+
click.echo("Auto-syncing...")
|
|
102
|
+
subprocess.run(["ap", "sync"])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
afterpython_group.add_command(init)
|
|
106
|
+
afterpython_group.add_command(build)
|
|
107
|
+
afterpython_group.add_command(dev)
|
|
108
|
+
afterpython_group.add_command(update)
|
|
109
|
+
afterpython_group.add_command(check)
|
|
110
|
+
afterpython_group.add_command(check, name="lint")
|
|
111
|
+
afterpython_group.add_command(format)
|
|
112
|
+
afterpython_group.add_command(sync)
|
|
113
|
+
afterpython_group.add_command(start)
|
|
114
|
+
afterpython_group.add_command(install)
|
|
115
|
+
afterpython_group.add_command(doc)
|
|
116
|
+
afterpython_group.add_command(blog)
|
|
117
|
+
afterpython_group.add_command(tutorial)
|
|
118
|
+
afterpython_group.add_command(example)
|
|
119
|
+
afterpython_group.add_command(guide)
|
|
120
|
+
afterpython_group.add_command(preview)
|
|
121
|
+
afterpython_group.add_command(clean)
|
|
122
|
+
afterpython_group.add_command(pre_commit)
|
|
123
|
+
afterpython_group.add_command(pre_commit, name="pc")
|
|
124
|
+
afterpython_group.add_command(commitizen)
|
|
125
|
+
afterpython_group.add_command(commitizen, name="cz")
|
|
126
|
+
afterpython_group.add_command(commit)
|
|
127
|
+
afterpython_group.add_command(bump)
|
|
128
|
+
afterpython_group.add_command(release)
|
|
129
|
+
afterpython_group.add_command(init_branch_rules)
|
|
130
|
+
afterpython_group.add_command(add)
|
|
131
|
+
afterpython_group.add_command(remove)
|
|
132
|
+
afterpython_group.add_command(lock)
|
afterpython/const.py
ADDED
afterpython/main.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# import atexit
|
|
2
|
+
|
|
3
|
+
from afterpython.cli import afterpython_group
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def exit_cli():
|
|
7
|
+
"""Application Exitpoint."""
|
|
8
|
+
print("Cleanup actions here...")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_cli() -> None:
|
|
12
|
+
"""Application Entrypoint."""
|
|
13
|
+
# atexit.register(exit_cli)
|
|
14
|
+
afterpython_group(obj={})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_pcu() -> None:
|
|
18
|
+
"""Entrypoint for 'pcu' command (alias for 'ap update deps')."""
|
|
19
|
+
from afterpython.cli.commands.update import dependencies
|
|
20
|
+
|
|
21
|
+
dependencies()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
run_cli()
|