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
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from afterpython._typing import NodeEnv
6
+
7
+ import subprocess
8
+
9
+ import click
10
+ from click.exceptions import Exit
11
+
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
+ }
20
+
21
+
22
+ @click.command(**command_kwargs)
23
+ @click.pass_context
24
+ @click.option(
25
+ "--doc", is_flag=True, help="Start the development server for doc/ directory"
26
+ )
27
+ @click.option(
28
+ "--blog", is_flag=True, help="Start the development server for blog/ directory"
29
+ )
30
+ @click.option(
31
+ "--tutorial",
32
+ is_flag=True,
33
+ help="Start the development server for tutorial/ directory",
34
+ )
35
+ @click.option(
36
+ "--example",
37
+ is_flag=True,
38
+ help="Start the development server for example/ directory",
39
+ )
40
+ @click.option(
41
+ "--guide", is_flag=True, help="Start the development server for guide/ directory"
42
+ )
43
+ def start(ctx, doc: bool, blog: bool, tutorial: bool, example: bool, guide: bool):
44
+ """Simple wrapper for `myst start` for convenience"""
45
+ from afterpython.utils import find_node_env, has_content_for_myst
46
+ from afterpython.const import CONTENT_TYPES
47
+ from afterpython.utils import handle_passthrough_help
48
+
49
+ # Show both our options and myst's help and exit
50
+ handle_passthrough_help(
51
+ ctx,
52
+ ["myst", "start"],
53
+ show_underlying=True,
54
+ )
55
+
56
+ node_env: NodeEnv = find_node_env()
57
+ paths = ctx.obj["paths"]
58
+
59
+ if doc:
60
+ content_type = "doc"
61
+ elif blog:
62
+ content_type = "blog"
63
+ elif tutorial:
64
+ content_type = "tutorial"
65
+ elif example:
66
+ content_type = "example"
67
+ elif guide:
68
+ content_type = "guide"
69
+ else:
70
+ raise click.ClickException("No content type specified")
71
+
72
+ assert content_type in CONTENT_TYPES, f"Invalid content type: {content_type}"
73
+
74
+ # assert only one of the options is True
75
+ if sum([doc, blog, tutorial, example, guide]) != 1:
76
+ raise click.ClickException("Only one content type can be specified")
77
+
78
+ content_path = paths.afterpython_path / content_type
79
+
80
+ # Check if directory has any content files
81
+ if not has_content_for_myst(content_path):
82
+ click.echo(f"Skipping {content_type}/ (no content files found)")
83
+ return
84
+
85
+ result = subprocess.run(
86
+ ["myst", "start", *ctx.args], cwd=content_path, env=node_env, check=False
87
+ )
88
+ if result.returncode != 0:
89
+ raise Exit(result.returncode)
90
+
91
+
92
+ def _run(ctx):
93
+ # Get the name of the function that called _run
94
+ import inspect
95
+
96
+ caller_frame = inspect.currentframe().f_back
97
+ command_name = (
98
+ caller_frame.f_code.co_name
99
+ ) # e.g. doc, blog, tutorial, example, guide
100
+
101
+ # Create a new context for start that includes extra args for `myst start`
102
+ with start.make_context(
103
+ "start", [f"--{command_name}", *ctx.args], parent=ctx.parent
104
+ ) as start_ctx:
105
+ return start.invoke(start_ctx)
106
+
107
+
108
+ @click.command(**command_kwargs)
109
+ @click.pass_context
110
+ def doc(ctx):
111
+ """Start the development server for doc/ directory (equivalent to: start --doc)"""
112
+ _run(ctx)
113
+
114
+
115
+ @click.command(**command_kwargs)
116
+ @click.pass_context
117
+ def blog(ctx):
118
+ """Start the development server for blog/ directory (equivalent to: start --blog)"""
119
+ _run(ctx)
120
+
121
+
122
+ @click.command(**command_kwargs)
123
+ @click.pass_context
124
+ def tutorial(ctx):
125
+ """Start the development server for tutorial/ directory (equivalent to: start --tutorial)"""
126
+ _run(ctx)
127
+
128
+
129
+ @click.command(**command_kwargs)
130
+ @click.pass_context
131
+ def example(ctx):
132
+ """Start the development server for example/ directory (equivalent to: start --example)"""
133
+ _run(ctx)
134
+
135
+
136
+ @click.command(**command_kwargs)
137
+ @click.pass_context
138
+ def guide(ctx):
139
+ """Start the development server for guide/ directory (equivalent to: start --guide)"""
140
+ _run(ctx)
@@ -0,0 +1,108 @@
1
+ from datetime import datetime
2
+
3
+ import click
4
+ from pyproject_metadata import StandardMetadata
5
+
6
+ import afterpython as ap
7
+ from afterpython.utils import convert_author_name_to_id
8
+
9
+
10
+ def _sync_authors_yml(authors: list[tuple[str, str | None]]):
11
+ """Sync authors.yml with authors in pyproject.toml"""
12
+ from afterpython.tools.myst import update_authors_yml
13
+ from afterpython._io.yaml import read_yaml
14
+
15
+ # read myst.yml from docs path to get "version"
16
+ doc_myst_yml = read_yaml(ap.paths.afterpython_path / "doc" / "myst.yml")
17
+ data = {
18
+ "version": doc_myst_yml["version"],
19
+ "project": {
20
+ "contributors": [
21
+ # NOTE: author is a tuple of (name, email), so author[0] is the name and author[1] is the email
22
+ {
23
+ "id": convert_author_name_to_id(str(author[0])),
24
+ "name": str(author[0]),
25
+ "email": str(author[1]),
26
+ # "github": ...
27
+ }
28
+ for author in authors
29
+ ]
30
+ },
31
+ }
32
+ update_authors_yml(data)
33
+ click.echo("✓ Synced authors.yml with pyproject.toml")
34
+
35
+
36
+ @click.command()
37
+ def sync():
38
+ """Sync between pyproject.toml+afterpython.toml and authors.yml+myst.yml files"""
39
+ from afterpython.const import CONTENT_TYPES
40
+ from afterpython.tools._afterpython import read_afterpython
41
+ from afterpython.tools.pyproject import read_metadata
42
+ from afterpython._io.toml import _from_tomlkit
43
+ from afterpython.tools.myst import update_myst_yml
44
+
45
+ pyproject: StandardMetadata = read_metadata()
46
+ project_name = str(pyproject.name)
47
+ afterpython = read_afterpython()
48
+ github_url = str(pyproject.urls.get("repository", ""))
49
+ company_name = str(_from_tomlkit(afterpython["company"]).get("name", ""))
50
+ company_url = str(_from_tomlkit(afterpython["company"]).get("url", ""))
51
+ website_url = str(_from_tomlkit(afterpython["website"]).get("url", ""))
52
+ authors = pyproject.authors
53
+ nav_bar = [
54
+ {
55
+ "title": content_type.capitalize() + "s",
56
+ "url": f"{website_url}/{content_type}",
57
+ }
58
+ if website_url
59
+ else ""
60
+ for content_type in CONTENT_TYPES
61
+ ]
62
+
63
+ _sync_authors_yml(authors)
64
+
65
+ # update myst.yml files for each content type (e.g. doc/, blog/, tutorial/, example/, guide/)
66
+ # based on the current values in pyproject.toml and afterpython.toml
67
+ for content_type in CONTENT_TYPES:
68
+ path = ap.paths.afterpython_path / content_type
69
+ nav_bar_per_content_type = [
70
+ item for item in nav_bar if item["title"] != content_type.capitalize()
71
+ ]
72
+ title = project_name + f"'s {content_type.capitalize()}"
73
+ data = {
74
+ "project": {
75
+ # using author ids defined in authors.yml
76
+ "authors": [
77
+ convert_author_name_to_id(str(author[0])) for author in authors
78
+ ],
79
+ "venue": {
80
+ # NOTE: company's name is used as the venue title
81
+ "title": company_name,
82
+ "url": company_url,
83
+ },
84
+ "copyright": f"© {company_name or project_name} {datetime.now().year}. All rights reserved.",
85
+ "title": "",
86
+ "description": str(pyproject.description),
87
+ "keywords": list(map(str, pyproject.keywords)),
88
+ "github": github_url,
89
+ },
90
+ "site": {
91
+ "title": title,
92
+ "options": {
93
+ "logo_text": project_name,
94
+ "logo_url": website_url,
95
+ },
96
+ "nav": nav_bar_per_content_type,
97
+ "actions": [
98
+ {
99
+ "title": "⭐ Star",
100
+ "url": github_url,
101
+ }
102
+ ],
103
+ },
104
+ }
105
+ update_myst_yml(data, path)
106
+ click.echo(
107
+ f"✓ Synced myst.yml in {path.name}/ with pyproject.toml and afterpython.toml"
108
+ )
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from afterpython._typing import NodeEnv
6
+ from afterpython.pcu import Dependencies
7
+
8
+ import subprocess
9
+ import shutil
10
+
11
+ import click
12
+ from click.exceptions import Exit
13
+
14
+
15
+ @click.group()
16
+ def update():
17
+ """Update website template, pyproject.toml dependencies, etc."""
18
+ pass
19
+
20
+
21
+ @update.command()
22
+ @click.option(
23
+ "-u",
24
+ "--upgrade",
25
+ is_flag=True,
26
+ help="if enabled, the dependencies will be upgraded to the latest version",
27
+ )
28
+ @click.option(
29
+ "--all",
30
+ is_flag=True,
31
+ help="update all versions in dependencies and tool configurations (e.g. pre-commit-config.yaml)",
32
+ )
33
+ def dependencies(upgrade: bool, all: bool):
34
+ """Update pyproject.toml dependencies to the latest version"""
35
+ from afterpython.pcu import get_dependencies, update_dependencies
36
+ from afterpython.utils import has_uv
37
+
38
+ dependencies: Dependencies = get_dependencies()
39
+ has_at_least_one_update = False
40
+ for dep_type in dependencies:
41
+ if not len(dependencies[dep_type]):
42
+ continue
43
+ click.echo(f"- {dep_type} package(s):")
44
+ # category = extras or group name
45
+ for category, deps in dependencies[dep_type].items():
46
+ if dep_type in ["dependencies", "build-system"]:
47
+ category_name = ""
48
+ elif dep_type == "optional-dependencies":
49
+ category_name = f"extras: {category}"
50
+ elif dep_type == "dependency-groups":
51
+ category_name = f"group: {category}"
52
+ else:
53
+ raise ValueError(f"Invalid dependency type: {dep_type}")
54
+ for dep in deps:
55
+ msg = f" {dep.requirement.name}: {dep.min_version}"
56
+ has_update = dep.min_version != dep.latest_version
57
+ if has_update:
58
+ has_at_least_one_update = True
59
+ msg += (
60
+ f" → {click.style(dep.latest_version, fg='green', bold=True)}"
61
+ )
62
+ if category_name:
63
+ msg += f" ({category_name})"
64
+ click.echo(msg)
65
+ if not has_at_least_one_update:
66
+ click.echo(f"\n{click.style('No dependencies to update.', bold=True)}")
67
+ return
68
+ if has_at_least_one_update and upgrade:
69
+ update_dependencies(dependencies) # write the latest versions to pyproject.toml
70
+ if has_uv():
71
+ click.echo("Upgrading dependencies with uv...")
72
+ result = subprocess.run(["uv", "lock"], check=False)
73
+ if result.returncode != 0:
74
+ raise Exit(result.returncode)
75
+ result = subprocess.run(
76
+ ["uv", "sync", "--all-extras", "--all-groups"], check=False
77
+ )
78
+ if result.returncode != 0:
79
+ raise Exit(result.returncode)
80
+ click.echo(
81
+ click.style(
82
+ "✓ All dependencies upgraded successfully 🎉", fg="green", bold=True
83
+ )
84
+ )
85
+ else:
86
+ click.echo(
87
+ "uv not found. Updated pyproject.toml only (packages not installed)."
88
+ )
89
+ if all:
90
+ subprocess.run(["ap", "pre-commit", "autoupdate"])
91
+ click.echo("All pre-commit hooks updated successfully.")
92
+
93
+
94
+ update.add_command(dependencies, name="deps") # alias for "dependencies"
95
+
96
+
97
+ @update.command()
98
+ @click.pass_context
99
+ @click.option(
100
+ "--no-backup",
101
+ is_flag=True,
102
+ help="if enabled, the existing project website template will not be backed up",
103
+ )
104
+ def website(ctx, no_backup: bool):
105
+ """Update the project website template to the latest version"""
106
+ from afterpython.utils import find_node_env
107
+
108
+ website_template_repo = "AfterPythonOrg/project-website-template"
109
+
110
+ paths = ctx.obj["paths"]
111
+ website_path = paths.website_path
112
+ if not no_backup:
113
+ backup_path = website_path.parent / "_website.backup"
114
+ if backup_path.exists():
115
+ click.echo(f"Removing old backup at {backup_path}...")
116
+ shutil.rmtree(backup_path)
117
+ if website_path.exists():
118
+ click.echo(f"Creating backup at {backup_path}...")
119
+ shutil.copytree(
120
+ website_path,
121
+ backup_path,
122
+ ignore=shutil.ignore_patterns("node_modules", ".svelte-kit"),
123
+ )
124
+
125
+ # Remove old template (but keep node_modules for faster reinstall)
126
+ if website_path.exists():
127
+ click.echo("Removing old project website template...")
128
+ shutil.rmtree(website_path)
129
+ website_path.mkdir(parents=True, exist_ok=True)
130
+
131
+ try:
132
+ click.echo("Updating the project website template...")
133
+ node_env: NodeEnv = find_node_env()
134
+ result = subprocess.run(
135
+ ["pnpx", "degit", website_template_repo, str(website_path)],
136
+ env=node_env,
137
+ check=False,
138
+ )
139
+ if result.returncode != 0:
140
+ raise Exit(result.returncode)
141
+ result = subprocess.run(
142
+ ["pnpm", "install"], cwd=website_path, env=node_env, check=False
143
+ )
144
+ if result.returncode != 0:
145
+ raise Exit(result.returncode)
146
+ except Exception as e:
147
+ click.echo(f"✗ Error updating project website template: {e}", err=True)
148
+ if not no_backup:
149
+ click.echo("Restoring from backup...")
150
+ if website_path.exists():
151
+ shutil.rmtree(website_path)
152
+ shutil.copytree(backup_path, website_path)
153
+ raise
@@ -0,0 +1,71 @@
1
+ import os
2
+ import subprocess
3
+
4
+ import click
5
+ from trogon import tui
6
+ from dotenv import load_dotenv, find_dotenv
7
+
8
+ import afterpython as ap
9
+ from afterpython import __version__
10
+ from afterpython.cli.commands.init import init
11
+ from afterpython.cli.commands.build import build
12
+ from afterpython.cli.commands.dev import dev
13
+ from afterpython.cli.commands.update import update
14
+ from afterpython.cli.commands.check import check
15
+ from afterpython.cli.commands.install import install
16
+ from afterpython.cli.commands.format import format
17
+ from afterpython.cli.commands.sync import sync
18
+ from afterpython.cli.commands.start import start, doc, blog, tutorial, example, guide
19
+ from afterpython.cli.commands.preview import preview
20
+ from afterpython.cli.commands.clean import clean
21
+ from afterpython.cli.commands.pre_commit import pre_commit
22
+ from afterpython.cli.commands.commitizen import commitizen
23
+ from afterpython.cli.commands.commit import commit
24
+ from afterpython.cli.commands.bump import bump
25
+ from afterpython.cli.commands.release import release
26
+
27
+
28
+ @tui(command="tui", help="Open terminal UI")
29
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
30
+ @click.pass_context
31
+ @click.version_option(version=__version__)
32
+ def afterpython_group(ctx):
33
+ """afterpython's CLI"""
34
+ load_dotenv(find_dotenv()) # Load environment variables from .env file
35
+ ctx.ensure_object(dict)
36
+ ctx.obj["paths"] = ap.paths
37
+
38
+ # Auto-sync before commands (except sync itself to avoid recursion)
39
+ if (
40
+ ctx.invoked_subcommand
41
+ and ctx.invoked_subcommand not in ["sync", "init"]
42
+ and os.getenv("AP_AUTO_SYNC", "0") == "1"
43
+ ):
44
+ click.echo("Auto-syncing...")
45
+ subprocess.run(["ap", "sync"])
46
+
47
+
48
+ afterpython_group.add_command(init)
49
+ afterpython_group.add_command(build)
50
+ afterpython_group.add_command(dev)
51
+ afterpython_group.add_command(update)
52
+ afterpython_group.add_command(check)
53
+ afterpython_group.add_command(format)
54
+ afterpython_group.add_command(sync)
55
+ afterpython_group.add_command(start)
56
+ afterpython_group.add_command(install)
57
+ afterpython_group.add_command(doc)
58
+ afterpython_group.add_command(blog)
59
+ afterpython_group.add_command(tutorial)
60
+ afterpython_group.add_command(example)
61
+ afterpython_group.add_command(guide)
62
+ afterpython_group.add_command(preview)
63
+ afterpython_group.add_command(clean)
64
+ afterpython_group.add_command(pre_commit)
65
+ afterpython_group.add_command(pre_commit, name="pc")
66
+ afterpython_group.add_command(pre_commit, name="precommit")
67
+ afterpython_group.add_command(commitizen)
68
+ afterpython_group.add_command(commitizen, name="cz")
69
+ afterpython_group.add_command(commit)
70
+ afterpython_group.add_command(bump)
71
+ afterpython_group.add_command(release)
afterpython/const.py ADDED
@@ -0,0 +1,5 @@
1
+ from afterpython._typing import tContentType
2
+
3
+
4
+ NODEENV_VERSION = "24.11.0"
5
+ CONTENT_TYPES: list[tContentType] = ["doc", "blog", "tutorial", "example", "guide"]
afterpython/cz.toml ADDED
@@ -0,0 +1,27 @@
1
+ [tool.commitizen]
2
+ name = "cz_conventional_commits"
3
+ # name = "cz_customize"
4
+ tag_format = "v$version"
5
+ version_scheme = "pep440"
6
+ version_provider = "uv"
7
+ update_changelog_on_bump = false
8
+ major_version_zero = true
9
+
10
+ # FIXME: commitizen can't even add a few custom types like "chore" and "wip", skip it for now
11
+ # [[tool.commitizen.customize.questions]]
12
+ # type = "list"
13
+ # name = "change_type"
14
+ # choices = [
15
+ # {value = "fix", name = "fix: A bug fix. Correlates with PATCH in SemVer"},
16
+ # {value = "feat", name = "feat: A new feature. Correlates with MINOR in SemVer"},
17
+ # {value = "docs", name = "docs: Documentation only changes"},
18
+ # {value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)"},
19
+ # {value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature"},
20
+ # {value = "perf", name = "perf: A code change that improves performance"},
21
+ # {value = "test", name = "test: Adding missing or correcting existing tests"},
22
+ # {value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)"},
23
+ # {value = "ci", name = "ci: Changes to CI configuration files and scripts (example scopes: GitLabCI)"},
24
+ # {value = "chore", name = "chore: Other changes that don't modify src or test files"},
25
+ # {value = "wip", name = "wip: Work in progress"}
26
+ # ]
27
+ # message = "Select the type of change you are committing"
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()