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.
Files changed (66) hide show
  1. afterpython/__init__.py +13 -0
  2. afterpython/_io/toml.py +105 -0
  3. afterpython/_io/yaml.py +26 -0
  4. afterpython/_paths.py +39 -0
  5. afterpython/_typing.py +4 -0
  6. afterpython/builders/__init__.py +17 -0
  7. afterpython/builders/content_json.py +131 -0
  8. afterpython/builders/index_md.py +94 -0
  9. afterpython/builders/jupyter_notebook.py +293 -0
  10. afterpython/builders/markdown.py +271 -0
  11. afterpython/builders/metadata.py +50 -0
  12. afterpython/cli/__init__.py +3 -0
  13. afterpython/cli/commands/add.py +67 -0
  14. afterpython/cli/commands/build.py +260 -0
  15. afterpython/cli/commands/bump.py +118 -0
  16. afterpython/cli/commands/check.py +36 -0
  17. afterpython/cli/commands/clean.py +85 -0
  18. afterpython/cli/commands/commit.py +52 -0
  19. afterpython/cli/commands/commitizen.py +32 -0
  20. afterpython/cli/commands/dev.py +213 -0
  21. afterpython/cli/commands/format.py +28 -0
  22. afterpython/cli/commands/init.py +107 -0
  23. afterpython/cli/commands/init_branch_rules.py +9 -0
  24. afterpython/cli/commands/install.py +28 -0
  25. afterpython/cli/commands/lock.py +23 -0
  26. afterpython/cli/commands/pre_commit.py +66 -0
  27. afterpython/cli/commands/preview.py +22 -0
  28. afterpython/cli/commands/release.py +67 -0
  29. afterpython/cli/commands/remove.py +67 -0
  30. afterpython/cli/commands/start.py +144 -0
  31. afterpython/cli/commands/sync.py +152 -0
  32. afterpython/cli/commands/update.py +176 -0
  33. afterpython/cli/main.py +132 -0
  34. afterpython/const.py +4 -0
  35. afterpython/main.py +25 -0
  36. afterpython/pcu.py +166 -0
  37. afterpython/templates/ci-workflow-template.yml +197 -0
  38. afterpython/templates/cz-template.toml +7 -0
  39. afterpython/templates/dependabot-template.yml +30 -0
  40. afterpython/templates/deploy-workflow-template.yml +88 -0
  41. afterpython/templates/pre-commit-config-template.yaml +48 -0
  42. afterpython/templates/release-workflow-template.yml +56 -0
  43. afterpython/templates/ruff-template.toml +32 -0
  44. afterpython/tools/_afterpython.py +128 -0
  45. afterpython/tools/_git.py +137 -0
  46. afterpython/tools/branch_rules.py +106 -0
  47. afterpython/tools/commitizen.py +13 -0
  48. afterpython/tools/github_actions.py +48 -0
  49. afterpython/tools/myst.py +208 -0
  50. afterpython/tools/pre_commit.py +37 -0
  51. afterpython/tools/pyproject.py +110 -0
  52. afterpython/utils.py +299 -0
  53. afterpython/version.py +39 -0
  54. afterpython-0.1.0.dist-info/METADATA +131 -0
  55. afterpython-0.1.0.dist-info/RECORD +57 -0
  56. afterpython-0.1.0.dist-info/WHEEL +4 -0
  57. afterpython-0.1.0.dist-info/entry_points.txt +4 -0
  58. afterpython/README.py +0 -158
  59. afterpython/images/favicon.ico +0 -0
  60. afterpython/images/logo-trace.svg +0 -3
  61. afterpython/images/logo.png +0 -0
  62. afterpython/images/logo.svg +0 -2
  63. afterpython-0.0.1.dev2.dist-info/LICENSE +0 -201
  64. afterpython-0.0.1.dev2.dist-info/METADATA +0 -20
  65. afterpython-0.0.1.dev2.dist-info/RECORD +0 -9
  66. 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
@@ -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
@@ -0,0 +1,4 @@
1
+ from afterpython._typing import tContentType
2
+
3
+ NODEENV_VERSION = "24.11.0"
4
+ CONTENT_TYPES: set[tContentType] = {"doc", "blog", "tutorial", "example", "guide"}
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()