afterpython 0.0.1.dev2__py3-none-any.whl → 0.1.0.dev5__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 +190 -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 +90 -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.dev5.dist-info/METADATA +105 -0
  45. afterpython-0.1.0.dev5.dist-info/RECORD +47 -0
  46. afterpython-0.1.0.dev5.dist-info/WHEEL +4 -0
  47. afterpython-0.1.0.dev5.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,164 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from pathlib import Path
6
+ from afterpython._typing import tContentType, NodeEnv
7
+
8
+ import subprocess
9
+
10
+ import afterpython as ap
11
+ from afterpython.utils import deep_merge
12
+ from afterpython._io.yaml import write_yaml, read_yaml
13
+
14
+
15
+ def update_authors_yml(data_update: dict):
16
+ """Update authors.yml while preserving order and formatting"""
17
+ file_path = ap.paths.afterpython_path / "authors.yml"
18
+
19
+ if not file_path.exists():
20
+ write_yaml(file_path, data_update)
21
+ return
22
+
23
+ existing_data = read_yaml(file_path) or {}
24
+
25
+ # merge authors, keep the author with more fields (e.g. github, x, etc.) to avoid duplication
26
+ merged_authors = {}
27
+ for author in (
28
+ existing_data["project"]["contributors"]
29
+ + data_update["project"]["contributors"]
30
+ ):
31
+ author_id = author["id"]
32
+ if author_id not in merged_authors:
33
+ merged_authors[author_id] = author
34
+ else:
35
+ # keep the author with more fields (e.g. github, x, etc.)
36
+ if len(author.keys()) > len(merged_authors[author_id].keys()):
37
+ merged_authors[author_id] = author
38
+ existing_data["project"]["contributors"] = list(merged_authors.values())
39
+
40
+ # set comments for project section for convenience
41
+ if "project" in existing_data and not existing_data["project"].ca.items.get(
42
+ "contributors"
43
+ ):
44
+ existing_data["project"].yaml_set_comment_before_after_key(
45
+ "contributors",
46
+ before="See more at: https://mystmd.org/guide/frontmatter#frontmatter-authors",
47
+ )
48
+ write_yaml(file_path, existing_data)
49
+
50
+
51
+ def update_myst_yml(data_update: dict, path: Path):
52
+ """Update myst.yml while preserving order and formatting
53
+
54
+ Args:
55
+ data_update: dict of data to update
56
+ path: path to the myst.yml file, e.g. doc/, blog/, tutorial/, example/, guide/
57
+ """
58
+
59
+ file_path = path / "myst.yml"
60
+
61
+ if not file_path.exists():
62
+ raise FileNotFoundError(
63
+ f"myst.yml not found at {file_path}, did you forget to run `ap init` or `myst init`?"
64
+ )
65
+
66
+ existing_data = read_yaml(file_path) or {}
67
+ existing_data = deep_merge(existing_data, data_update)
68
+
69
+ # set comments for project section for convenience
70
+ if "project" in existing_data:
71
+ if not existing_data["project"].ca.items.get("id"):
72
+ existing_data["project"].yaml_set_comment_before_after_key(
73
+ "id",
74
+ before="See how to create Table of Contents at: https://mystmd.org/guide/table-of-contents",
75
+ )
76
+ if not existing_data["project"].ca.items.get("authors"):
77
+ existing_data["project"].yaml_set_comment_before_after_key(
78
+ "authors",
79
+ before="See more authors' fields at: https://mystmd.org/guide/frontmatter#frontmatter-authors",
80
+ )
81
+ if not existing_data["project"].ca.items.get("venue"):
82
+ existing_data["project"].yaml_set_comment_before_after_key(
83
+ "venue",
84
+ before="See more venue's fields at: https://mystmd.org/guide/frontmatter#venue",
85
+ )
86
+ if "site" in existing_data:
87
+ if not existing_data["site"].ca.items.get("options"):
88
+ existing_data["site"].yaml_set_comment_before_after_key(
89
+ "options",
90
+ before="See options at: https://mystmd.org/guide/website-templates#site-options",
91
+ )
92
+ if not existing_data["site"].ca.items.get("actions"):
93
+ existing_data["site"].yaml_set_comment_before_after_key(
94
+ "actions",
95
+ before="See web layout at: https://mystmd.org/guide/website-navigation",
96
+ )
97
+
98
+ write_yaml(file_path, existing_data)
99
+
100
+
101
+ def _write_welcome_file(content_type: tContentType):
102
+ welcome_file = ap.paths.afterpython_path / content_type / "index.md"
103
+ if welcome_file.exists():
104
+ return
105
+ welcome_content = f"""# Welcome to AfterPython
106
+
107
+ Welcome to your project's {content_type}! This is a starter page to help you get started.
108
+
109
+ ## Getting Started
110
+
111
+ Replace this placeholder content with your own. Here's what you can do:
112
+
113
+ - Creating new `.md` or `.ipynb` files in the `afterpython/{content_type}/` directory
114
+ - Writing in MyST Markdown format
115
+ - Adding images to the `afterpython/static/` directory and referencing them
116
+
117
+ ## Resources
118
+
119
+ - [AfterPython's Project Website](https://ap.afterpython.org)
120
+ - [MyST Markdown Guide](https://mystmd.org)
121
+
122
+ Start building your amazing project! 🚀
123
+ """
124
+ welcome_file.write_text(welcome_content)
125
+ return welcome_file
126
+
127
+
128
+ def init_myst():
129
+ """
130
+ Initialize MyST Markdown (mystmd) and myst.yml files in
131
+ doc/, blog/, tutorial/, example/, guide/ directories with sensible defaults
132
+ """
133
+ from afterpython.const import CONTENT_TYPES
134
+ from afterpython.utils import find_node_env
135
+
136
+ # find any existing node.js version and use it, if no, install the Node.js version specified in NODEENV_VERSION
137
+ node_env: NodeEnv = find_node_env()
138
+ subprocess.run(["npm", "install", "-g", "pnpm"], env=node_env, check=True)
139
+ for content_type in CONTENT_TYPES:
140
+ path = ap.paths.afterpython_path / content_type
141
+ print(f"Initializing MyST Markdown (mystmd) in {path.name}/ directory ...")
142
+ path.mkdir(parents=True, exist_ok=True)
143
+ subprocess.run(["myst", "init"], cwd=path, input="n\n", text=True, env=node_env)
144
+ myst_yml_defaults = {
145
+ "extends": "../authors.yml",
146
+ "project": {
147
+ "license": "CC-BY-4.0",
148
+ "subject": content_type.capitalize()
149
+ if content_type != "doc"
150
+ else "Documentation",
151
+ },
152
+ "site": {
153
+ "options": {
154
+ "favicon": "../static/favicon.ico",
155
+ "logo": "../static/logo.svg",
156
+ "logo_dark": "../static/logo.svg",
157
+ "analytics_google": "{{ GOOGLE_ANALYTICS_ID }}",
158
+ # "twitter": "",
159
+ },
160
+ },
161
+ }
162
+ update_myst_yml(myst_yml_defaults, path)
163
+ _write_welcome_file(content_type)
164
+ subprocess.run(["ap", "sync"])
@@ -0,0 +1,62 @@
1
+ import subprocess
2
+
3
+ import afterpython as ap
4
+
5
+ from afterpython._io.yaml import write_yaml, read_yaml
6
+
7
+
8
+ pre_commit_default = {
9
+ # default is only ["pre-commit"], without changing the default,
10
+ # we need to explicitly pass in --hook-type whenever we run "pre-commit install --hook-type ..."
11
+ "default_install_hook_types": ["pre-commit", "commit-msg", "pre-push"],
12
+ "exclude": "^afterpython/_website/", # do not check the project website template
13
+ "repos": [
14
+ {
15
+ "repo": "https://github.com/pre-commit/pre-commit-hooks",
16
+ "rev": "v6.0.0",
17
+ "hooks": [
18
+ {"id": "trailing-whitespace", "stages": ["pre-commit"]},
19
+ {"id": "end-of-file-fixer", "stages": ["pre-commit"]},
20
+ {"id": "check-yaml", "stages": ["pre-commit"]},
21
+ {"id": "check-toml", "stages": ["pre-commit"]},
22
+ {"id": "check-added-large-files", "stages": ["pre-commit"]},
23
+ ],
24
+ },
25
+ # {
26
+ # "repo": "https://github.com/astral-sh/uv-pre-commit",
27
+ # "rev": "0.9.8",
28
+ # "hooks": [
29
+ # {"id": "uv-lock"}, # Update the uv lockfile
30
+ # ]
31
+ # },
32
+ ],
33
+ }
34
+
35
+
36
+ def install_pre_commit():
37
+ # installed in .git/hooks
38
+ subprocess.run(["ap", "pre-commit", "install", "--install-hooks"], check=True)
39
+
40
+
41
+ def update_pre_commit(data_update: dict):
42
+ from afterpython.utils import deep_merge
43
+
44
+ pre_commit_path = ap.paths.afterpython_path / ".pre-commit-config.yaml"
45
+ if not pre_commit_path.exists():
46
+ raise FileNotFoundError(
47
+ f".pre-commit-config.yaml not found at {pre_commit_path}"
48
+ )
49
+ existing_data = read_yaml(pre_commit_path)
50
+ existing_data = deep_merge(existing_data, data_update)
51
+ write_yaml(pre_commit_path, existing_data)
52
+ install_pre_commit()
53
+
54
+
55
+ def init_pre_commit():
56
+ pre_commit_path = ap.paths.afterpython_path / ".pre-commit-config.yaml"
57
+ if pre_commit_path.exists():
58
+ print(f".pre-commit-config.yaml already exists at {pre_commit_path}")
59
+ return
60
+ write_yaml(pre_commit_path, pre_commit_default)
61
+ print(f"Created {pre_commit_path}")
62
+ install_pre_commit()
@@ -0,0 +1,72 @@
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 asyncio
8
+
9
+ from pyproject_metadata import StandardMetadata
10
+
11
+ import afterpython as ap
12
+ from afterpython._io.toml import read_toml, write_toml, _to_tomlkit
13
+
14
+
15
+ def read_pyproject() -> TOMLDocument:
16
+ """Read pyproject.toml"""
17
+ return read_toml(ap.paths.pyproject_path)
18
+
19
+
20
+ def write_pyproject(data: TOMLDocument):
21
+ write_toml(ap.paths.pyproject_path, data)
22
+
23
+
24
+ def read_metadata() -> StandardMetadata:
25
+ """Read metadata from pyproject.toml"""
26
+ return StandardMetadata.from_pyproject(read_pyproject())
27
+
28
+
29
+ def init_pyproject():
30
+ """Initialize pyproject.toml with sensible defaults
31
+ - add [build-system] section with uv build backend (same as `uv init --package`)
32
+ - add [project.urls] section with homepage, repository, and documentation URLs
33
+ """
34
+ import httpx
35
+ from afterpython.utils import fetch_pypi_json
36
+ from afterpython.tools._git import get_git_user_config, get_github_url
37
+
38
+ build_backend = "uv_build"
39
+
40
+ async def fetch_build_backend_version() -> str | None:
41
+ """Fetch the latest version of build backend package from PyPI."""
42
+ async with httpx.AsyncClient() as client:
43
+ data = await fetch_pypi_json(client, build_backend)
44
+ return data["info"]["version"] if data else None
45
+
46
+ data: TOMLDocument = read_pyproject()
47
+ is_updated = False
48
+
49
+ if "build-system" not in data:
50
+ uv_build_version = asyncio.run(fetch_build_backend_version())
51
+ if uv_build_version:
52
+ data["build-system"] = {
53
+ "requires": [f"{build_backend}>={uv_build_version}"],
54
+ "build-backend": build_backend,
55
+ }
56
+ is_updated = True
57
+
58
+ if "project" in data:
59
+ if "urls" not in data["project"]:
60
+ data["project"]["urls"] = {
61
+ "homepage": "",
62
+ "repository": get_github_url() or "",
63
+ "documentation": "",
64
+ }
65
+ is_updated = True
66
+ if "authors" not in data["project"]:
67
+ # convert git user config to tomlkit object
68
+ data["project"]["authors"] = _to_tomlkit([get_git_user_config()])
69
+ is_updated = True
70
+
71
+ if is_updated:
72
+ write_pyproject(data)
afterpython/utils.py ADDED
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from pathlib import Path
6
+ from httpx import AsyncClient
7
+ from afterpython._typing import NodeEnv
8
+
9
+ import os
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+
14
+ import click
15
+
16
+
17
+ def find_node_env() -> NodeEnv:
18
+ """
19
+ Find if there is an installed Node.js version, if yes, use it
20
+ If no, install the Node.js version specified in NODEENV_VERSION
21
+ """
22
+ from mystmd_py.nodeenv import find_any_node
23
+ from afterpython.const import NODEENV_VERSION
24
+
25
+ # from mystmd_py.main import ensure_valid_version
26
+ # Use mystmd's own node-finding logic
27
+ binary_path = os.environ.get("PATH", os.defpath)
28
+ _node_path, os_path = find_any_node(binary_path, nodeenv_version=NODEENV_VERSION)
29
+ node_env: NodeEnv = {**os.environ, "PATH": os_path}
30
+ return node_env
31
+
32
+
33
+ def has_uv() -> bool:
34
+ """Check if uv is installed"""
35
+ return shutil.which("uv") is not None
36
+
37
+
38
+ def has_gh() -> bool:
39
+ """Check if gh is installed"""
40
+ return shutil.which("gh") is not None
41
+
42
+
43
+ async def fetch_pypi_json(client: AsyncClient, package_name: str) -> dict | None:
44
+ url = f"https://pypi.org/pypi/{package_name}/json"
45
+ try:
46
+ response = await client.get(url)
47
+ response.raise_for_status()
48
+ data = response.json()
49
+ return data
50
+ except Exception as e:
51
+ # Return None if package doesn't exist or network fails
52
+ print(f"Warning: Could not fetch PyPI JSON for {package_name}: {e}")
53
+ return None
54
+
55
+
56
+ # VIBE-CODED
57
+ def detect_license_from_file(file_path: str) -> str:
58
+ """Extract license name from LICENSE file using regex."""
59
+ with open(file_path, encoding="utf-8") as f:
60
+ content = f.read()
61
+
62
+ # Common license patterns
63
+ patterns = {
64
+ "Apache-2.0": r"Apache License\s+Version 2\.0",
65
+ "MIT": r"MIT License|Permission is hereby granted, free of charge",
66
+ "GPL-3.0": r"GNU GENERAL PUBLIC LICENSE\s+Version 3",
67
+ "GPL-2.0": r"GNU GENERAL PUBLIC LICENSE\s+Version 2",
68
+ "BSD-3-Clause": r"BSD 3-Clause License|Redistribution and use in source and binary forms",
69
+ "BSD-2-Clause": r"BSD 2-Clause License",
70
+ "ISC": r"ISC License",
71
+ "LGPL-3.0": r"GNU LESSER GENERAL PUBLIC LICENSE\s+Version 3",
72
+ "MPL-2.0": r"Mozilla Public License Version 2\.0",
73
+ }
74
+
75
+ # Check first 500 chars for license header
76
+ header = content[:500]
77
+
78
+ for license_id, pattern in patterns.items():
79
+ if re.search(pattern, header, re.IGNORECASE):
80
+ return license_id
81
+
82
+ return ""
83
+
84
+
85
+ def deep_merge(base: dict, updates: dict) -> dict:
86
+ """Deep merge updates into base, preserving structure and metadata.
87
+
88
+ Works with:
89
+ - Regular Python dicts
90
+ - tomlkit TOMLDocument/Table objects
91
+ - ruamel.yaml CommentedMap objects
92
+
93
+ Behavior:
94
+ - Dicts are merged recursively
95
+ - Lists are extended (concatenated)
96
+ - Other types are overwritten
97
+
98
+ Args:
99
+ base: The base dictionary to merge into (modified in-place)
100
+ updates: The updates to apply
101
+
102
+ Returns:
103
+ The merged base dictionary (same object, modified in-place)
104
+ """
105
+ for key, value in updates.items():
106
+ if key in base:
107
+ if isinstance(base[key], dict) and isinstance(value, dict):
108
+ # Recursively merge dicts
109
+ deep_merge(base[key], value)
110
+ elif isinstance(base[key], list) and isinstance(value, list):
111
+ # Extend lists, but only add items that don't already exist
112
+ for item in value:
113
+ if item not in base[key]:
114
+ base[key].append(item)
115
+ else:
116
+ # Overwrite for other types
117
+ base[key] = value
118
+ else:
119
+ base[key] = value
120
+
121
+ return base
122
+
123
+
124
+ def convert_author_name_to_id(name: str) -> str:
125
+ """Convert author name to ID
126
+
127
+ Args:
128
+ name: The author name
129
+
130
+ Returns:
131
+ The author ID
132
+
133
+ Examples:
134
+ - "Stephen Yau" -> "stephen_yau"
135
+ - "John Doe" -> "john_doe"
136
+ - "Jane Smith" -> "jane_smith"
137
+ """
138
+ return name.replace(" ", "_").lower()
139
+
140
+
141
+ def find_available_port(
142
+ start_port: int = 3000, max_port: int = 3100, host: str = "localhost"
143
+ ) -> int:
144
+ """
145
+ Find a TCP port with no listener on `host`, starting from start_port.
146
+ """
147
+ import socket
148
+
149
+ def is_port_in_use(
150
+ port: int, host: str = "localhost", timeout: float = 0.2
151
+ ) -> bool:
152
+ """
153
+ Return True if *any* TCP listener is active on the given host:port.
154
+ This works across IPv4/IPv6 by using getaddrinfo().
155
+ """
156
+ try:
157
+ for family, socktype, proto, _canonname, sockaddr in socket.getaddrinfo(
158
+ host, port, type=socket.SOCK_STREAM
159
+ ):
160
+ with socket.socket(family, socktype, proto) as s:
161
+ s.settimeout(timeout)
162
+ if s.connect_ex(sockaddr) == 0:
163
+ return True
164
+ except socket.gaierror:
165
+ # host couldn't be resolved; treat as no listener
166
+ return False
167
+ return False
168
+
169
+ for port in range(start_port, max_port + 1):
170
+ if not is_port_in_use(port, host=host):
171
+ return port
172
+ raise RuntimeError(
173
+ f"No free ports available in range {start_port}-{max_port} for host={host!r}"
174
+ )
175
+
176
+
177
+ def has_content_for_myst(content_path: Path) -> bool:
178
+ """Check if a content directory has any content files"""
179
+ return any(
180
+ content_path.glob("*.md")
181
+ or content_path.glob("*.ipynb")
182
+ or content_path.glob("*.tex")
183
+ )
184
+
185
+
186
+ def handle_passthrough_help(
187
+ ctx: click.Context,
188
+ underlying_command: list[str],
189
+ show_underlying: bool = True,
190
+ help_flags: tuple[str, ...] = ("--help", "-h"),
191
+ ) -> None:
192
+ """Handle --help for commands that pass through to underlying tools.
193
+
194
+ Shows both Click's help (custom options) and the underlying tool's help.
195
+
196
+ Args:
197
+ ctx: Click context
198
+ underlying_command: Command to show help for (e.g., ["cz", "bump"])
199
+ show_underlying: Whether to show underlying tool's help (default: True)
200
+ help_flags: Flags that trigger help display (default: ("--help", "-h"))
201
+ Use ("--help",) if -h is reserved by the underlying tool
202
+ """
203
+ # Check if any help flag is present
204
+ if any(flag in ctx.args for flag in help_flags):
205
+ # Show Click's help first (our custom options)
206
+ click.echo(ctx.get_help())
207
+
208
+ # Then show underlying tool's help if requested
209
+ if show_underlying:
210
+ click.echo("\n" + "=" * 60)
211
+ click.echo(f"Additional options from '{' '.join(underlying_command)}':")
212
+ click.echo("=" * 60 + "\n")
213
+ subprocess.run([*underlying_command, "--help"])
214
+
215
+ ctx.exit(0)
afterpython/version.py ADDED
@@ -0,0 +1,39 @@
1
+ from packaging.version import Version as BaseVersion
2
+
3
+
4
+ class Version(BaseVersion):
5
+ def next_breaking(self, level: str = "auto") -> "Version":
6
+ """Get the next version that would break compatibility.
7
+
8
+ Args:
9
+ level: "major", "minor", or "auto" (detect from current version)
10
+ - If major == 0: breaks on minor (0.x API is unstable)
11
+ - If major >= 1: breaks on major (semver stable)
12
+
13
+ Examples:
14
+ Version("0.9.7").next_breaking() -> Version("0.10.0")
15
+ Version("1.2.3").next_breaking() -> Version("2.0.0")
16
+ Version("1.2.3").next_breaking("minor") -> Version("1.3.0")
17
+ """
18
+ if level == "auto":
19
+ # Follow semver convention: 0.x.x breaks on minor, 1.x.x+ breaks on major
20
+ level = "minor" if self.major == 0 else "major"
21
+
22
+ if level == "major":
23
+ return Version(f"{self.major + 1}.0.0")
24
+ elif level == "minor":
25
+ return Version(f"{self.major}.{self.minor + 1}.0")
26
+ else:
27
+ raise ValueError(f"Invalid level: {level}. Use 'major', 'minor', or 'auto'")
28
+
29
+ def bump_major(self) -> "Version":
30
+ """Bump major version. Example: 1.2.3 -> 2.0.0"""
31
+ return Version(f"{self.major + 1}.0.0")
32
+
33
+ def bump_minor(self) -> "Version":
34
+ """Bump minor version. Example: 0.9.7 -> 0.10.0"""
35
+ return Version(f"{self.major}.{self.minor + 1}.0")
36
+
37
+ def bump_patch(self) -> "Version":
38
+ """Bump patch version. Example: 0.9.7 -> 0.9.8"""
39
+ return Version(f"{self.major}.{self.minor}.{self.micro + 1}")
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: afterpython
3
+ Version: 0.1.0.dev5
4
+ Summary: CLI tool for AfterPython
5
+ Keywords: package-maintenance,project-website,static-site-generator,developer-tools,documentation,cli-tool,ci/cd,mystmd,semantic-release,python-packaging
6
+ Author: Stephen Yau
7
+ Author-email: Stephen Yau <softwareentrepreneer+afterpython@gmail.com>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Build Tools
12
+ Classifier: Topic :: Software Development :: Documentation
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Dist: click>=8.3.1
18
+ Requires-Dist: trogon>=0.6.0
19
+ Requires-Dist: httpx>=0.28.1
20
+ Requires-Dist: tomlkit>=0.13.3
21
+ Requires-Dist: ruamel-yaml>=0.18.16
22
+ Requires-Dist: gitpython>=3.1.45
23
+ Requires-Dist: pygithub>=2.8.1
24
+ Requires-Dist: python-dotenv>=1.2.1
25
+ Requires-Dist: pyproject-metadata>=0.9.1
26
+ Requires-Dist: mystmd>=1.6.6
27
+ Requires-Dist: pdoc>=16.0.0
28
+ Requires-Dist: ruff>=0.14.5
29
+ Requires-Dist: pre-commit>=4.4.0
30
+ Requires-Dist: commitizen>=4.10.0
31
+ Requires-Python: >=3.11
32
+ Project-URL: documentation, https://ap.afterpython.org/doc
33
+ Project-URL: homepage, https://afterpython.org
34
+ Project-URL: repository, https://github.com/AfterPythonOrg/afterpython
35
+ Description-Content-Type: text/markdown
36
+
37
+ # AfterPython: Python Package Maintenance Toolkit and Project Website Generator
38
+
39
+ [![afterpython](https://afterpython.org/shield.svg)](https://afterpython.org)
40
+ [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)
41
+ ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython)
42
+ [![PyPI](https://img.shields.io/pypi/v/afterpython.svg)](https://pypi.org/project/afterpython)
43
+ ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython)
44
+ [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
45
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/AfterPythonOrg/afterpython)
46
+
47
+
48
+ [MyST]: https://mystmd.org
49
+ [MyST Markdown]: https://mystmd.org/spec/
50
+ [Jupyter Notebook]: https://jupyter.org
51
+ [pre-commit]: https://pre-commit.com
52
+ [pagefind]: https://pagefind.app
53
+ [SemVer]: https://semver.org
54
+ [pdoc]: https://pdoc.dev/
55
+ [GitHub Actions]: https://github.com/features/actions
56
+ [PyTorch]: https://pytorch.org
57
+ [Scikit-Learn]: https://scikit-learn.org
58
+ [WebLLM]: https://webllm.mlc.ai/
59
+ [project-website-template]: https://github.com/AfterPythonOrg/project-website-template
60
+ [uv]: https://docs.astral.sh/uv/
61
+ [ruff]: https://docs.astral.sh/ruff/
62
+
63
+
64
+ ## Problem
65
+ Going from **writing Python code to publishing and maintaining a package** is **time-consuming**.
66
+ First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [SemVer], CI/CD ([pre-commit] hooks, GitHub workflows), and they often spending hours just to deploy a documentation site whereas well-resourced projects like [PyTorch] and [Scikit-Learn] have **dedicated websites** that serve as their project hubs.
67
+
68
+
69
+ ## Solution
70
+ `afterpython` **automates** the tedious parts of Python package maintenance and generates a **project website** for **building community** and **hosting content** such as documentation, **blog posts**, tutorials, examples and more — empowering more developers to write packages with ease.
71
+
72
+ ---
73
+ `afterpython` is a CLI tool that **abstracts away the complexity** of **content writing, website deployment, and package release/maintenance** by providing an opinionated set of modern tools — so you don’t have to spend time selecting or learning anything beyond the basics.
74
+
75
+
76
+ ## Core Features
77
+ - [x] Write content directly in [MyST Markdown] or [Jupyter Notebook]
78
+ - [x] Go from writing to website deployment in minutes — no need to learn any of the underlying tools
79
+ - [x] Centralize all your content in a modern, unified project website — from documentation to blog posts
80
+ - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
81
+ - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
82
+ - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
83
+
84
+
85
+ ## Installation
86
+ ```bash
87
+ # install afterpython as a dev dependency
88
+ uv add --dev afterpython
89
+ # initialize afterpython
90
+ ap init
91
+ ```
92
+
93
+
94
+ ## Tech Stack
95
+ - [MyST]
96
+ - [project-website-template]
97
+ - [pre-commit]
98
+ - [GitHub Actions]
99
+ - [pdoc]
100
+ - [uv]
101
+ - [ruff]
102
+ - [pagefind]
103
+ - [WebLLM]
104
+ <!-- - ty -->
105
+ <!-- - [pixi] -->