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,110 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from tomlkit.toml_document import TOMLDocument
7
+
8
+ import asyncio
9
+ from pathlib import Path
10
+
11
+ from pyproject_metadata import StandardMetadata
12
+
13
+ import afterpython as ap
14
+ from afterpython._io.toml import _to_tomlkit, read_toml, write_toml
15
+
16
+
17
+ def read_pyproject() -> TOMLDocument:
18
+ """Read pyproject.toml"""
19
+ return read_toml(ap.paths.pyproject_path)
20
+
21
+
22
+ def write_pyproject(data: TOMLDocument):
23
+ write_toml(ap.paths.pyproject_path, data)
24
+
25
+
26
+ def read_metadata() -> StandardMetadata:
27
+ """Read metadata from pyproject.toml"""
28
+ return StandardMetadata.from_pyproject(read_pyproject())
29
+
30
+
31
+ def find_package_directory() -> Path:
32
+ """Find the user's package directory.
33
+
34
+ Supports both common Python project layouts:
35
+ - src layout: user_project/src/package_name/__init__.py
36
+ - flat layout: user_project/package_name/__init__.py
37
+
38
+ Returns:
39
+ Path to the package directory containing __init__.py
40
+
41
+ Raises:
42
+ FileNotFoundError: If package directory cannot be found in either layout
43
+ """
44
+ metadata = read_metadata()
45
+ package_name = metadata.name
46
+ project_root = ap.paths.user_path
47
+
48
+ # Try src layout first (recommended layout)
49
+ src_layout_dir = project_root / "src" / package_name
50
+ if (src_layout_dir / "__init__.py").exists():
51
+ return src_layout_dir
52
+
53
+ # Try flat layout
54
+ flat_layout_dir = project_root / package_name
55
+ if (flat_layout_dir / "__init__.py").exists():
56
+ return flat_layout_dir
57
+
58
+ # Package not found in either location
59
+ raise FileNotFoundError(
60
+ f"Could not find package '{package_name}' in either src layout "
61
+ f"({src_layout_dir}) or flat layout ({flat_layout_dir}). "
62
+ f"Expected to find __init__.py in one of these locations."
63
+ )
64
+
65
+
66
+ def init_pyproject():
67
+ """Initialize pyproject.toml with sensible defaults
68
+ - add [build-system] section with uv build backend (same as `uv init --package`)
69
+ - add [project.urls] section with homepage, repository, and documentation URLs
70
+ """
71
+ import httpx
72
+
73
+ from afterpython.tools._git import get_git_user_config, get_github_url
74
+ from afterpython.utils import fetch_pypi_json
75
+
76
+ build_backend = "uv_build"
77
+
78
+ async def fetch_build_backend_version() -> str | None:
79
+ """Fetch the latest version of build backend package from PyPI."""
80
+ async with httpx.AsyncClient() as client:
81
+ data = await fetch_pypi_json(client, build_backend)
82
+ return data["info"]["version"] if data else None
83
+
84
+ data: TOMLDocument = read_pyproject()
85
+ is_updated = False
86
+
87
+ if "build-system" not in data:
88
+ uv_build_version = asyncio.run(fetch_build_backend_version())
89
+ if uv_build_version:
90
+ data["build-system"] = {
91
+ "requires": [f"{build_backend}>={uv_build_version}"],
92
+ "build-backend": build_backend,
93
+ }
94
+ is_updated = True
95
+
96
+ if "project" in data:
97
+ if "urls" not in data["project"]:
98
+ data["project"]["urls"] = {
99
+ "homepage": "",
100
+ "repository": get_github_url() or "",
101
+ "documentation": "",
102
+ }
103
+ is_updated = True
104
+ if "authors" not in data["project"]:
105
+ # convert git user config to tomlkit object
106
+ data["project"]["authors"] = _to_tomlkit([get_git_user_config()])
107
+ is_updated = True
108
+
109
+ if is_updated:
110
+ write_pyproject(data)
afterpython/utils.py ADDED
@@ -0,0 +1,299 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from pathlib import Path
7
+
8
+ from httpx import AsyncClient
9
+
10
+ from afterpython._typing import NodeEnv
11
+
12
+ import os
13
+ import re
14
+ import shutil
15
+ import subprocess
16
+
17
+ import click
18
+
19
+
20
+ def find_node_env() -> NodeEnv:
21
+ """
22
+ Find if there is an installed Node.js version, if yes, use it
23
+ If no, install the Node.js version specified in NODEENV_VERSION
24
+ """
25
+ from mystmd_py.nodeenv import find_any_node
26
+
27
+ from afterpython.const import NODEENV_VERSION
28
+
29
+ # from mystmd_py.main import ensure_valid_version
30
+ # Use mystmd's own node-finding logic
31
+ binary_path = os.environ.get("PATH", os.defpath)
32
+ _node_path, os_path = find_any_node(binary_path, nodeenv_version=NODEENV_VERSION)
33
+ node_env: NodeEnv = {**os.environ, "PATH": os_path}
34
+ return node_env
35
+
36
+
37
+ def has_uv() -> bool:
38
+ """Check if uv is installed"""
39
+ return shutil.which("uv") is not None
40
+
41
+
42
+ def has_pixi() -> bool:
43
+ """Check if pixi is installed"""
44
+ return shutil.which("pixi") is not None
45
+
46
+
47
+ def has_gh() -> bool:
48
+ """Check if gh is installed"""
49
+ return shutil.which("gh") is not None
50
+
51
+
52
+ async def fetch_pypi_json(client: AsyncClient, package_name: str) -> dict | None:
53
+ url = f"https://pypi.org/pypi/{package_name}/json"
54
+ try:
55
+ response = await client.get(url)
56
+ response.raise_for_status()
57
+ data = response.json()
58
+ return data
59
+ except Exception as e:
60
+ # Return None if package doesn't exist or network fails
61
+ print(f"Warning: Could not fetch PyPI JSON for {package_name}: {e}")
62
+ return None
63
+
64
+
65
+ # VIBE-CODED
66
+ def detect_license_from_file(file_path: str) -> str:
67
+ """Extract license name from LICENSE file using regex."""
68
+ with open(file_path, encoding="utf-8") as f:
69
+ content = f.read()
70
+
71
+ # Common license patterns
72
+ patterns = {
73
+ "Apache-2.0": r"Apache License\s+Version 2\.0",
74
+ "MIT": r"MIT License|Permission is hereby granted, free of charge",
75
+ "GPL-3.0": r"GNU GENERAL PUBLIC LICENSE\s+Version 3",
76
+ "GPL-2.0": r"GNU GENERAL PUBLIC LICENSE\s+Version 2",
77
+ "BSD-3-Clause": r"BSD 3-Clause License|Redistribution and use in source and binary forms",
78
+ "BSD-2-Clause": r"BSD 2-Clause License",
79
+ "ISC": r"ISC License",
80
+ "LGPL-3.0": r"GNU LESSER GENERAL PUBLIC LICENSE\s+Version 3",
81
+ "MPL-2.0": r"Mozilla Public License Version 2\.0",
82
+ }
83
+
84
+ # Check first 500 chars for license header
85
+ header = content[:500]
86
+
87
+ for license_id, pattern in patterns.items():
88
+ if re.search(pattern, header, re.IGNORECASE):
89
+ return license_id
90
+
91
+ return ""
92
+
93
+
94
+ def deep_merge(base: dict, updates: dict, extend_lists: bool = True) -> dict:
95
+ """Deep merge updates into base, preserving structure and metadata.
96
+
97
+ Works with:
98
+ - Regular Python dicts
99
+ - tomlkit TOMLDocument/Table objects
100
+ - ruamel.yaml CommentedMap objects
101
+
102
+ Behavior:
103
+ - Dicts are merged recursively
104
+ - Lists are extended (concatenated)
105
+ - Other types are overwritten
106
+
107
+ Args:
108
+ base: The base dictionary to merge into (modified in-place)
109
+ updates: The updates to apply
110
+
111
+ Returns:
112
+ The merged base dictionary (same object, modified in-place)
113
+ """
114
+ for key, value in updates.items():
115
+ if key in base:
116
+ if isinstance(base[key], dict) and isinstance(value, dict):
117
+ # Recursively merge dicts
118
+ deep_merge(base[key], value, extend_lists=extend_lists)
119
+ elif (
120
+ isinstance(base[key], list) and isinstance(value, list) and extend_lists
121
+ ):
122
+ # Extend lists, but only add items that don't already exist
123
+ for item in value:
124
+ if item not in base[key]:
125
+ base[key].append(item)
126
+ else:
127
+ # Overwrite for other types
128
+ base[key] = value
129
+ else:
130
+ base[key] = value
131
+
132
+ return base
133
+
134
+
135
+ def convert_author_name_to_id(name: str) -> str:
136
+ """Convert author name to ID
137
+
138
+ Args:
139
+ name: The author name
140
+
141
+ Returns:
142
+ The author ID
143
+
144
+ Examples:
145
+ - "Stephen Yau" -> "stephen_yau"
146
+ - "John Doe" -> "john_doe"
147
+ - "Jane Smith" -> "jane_smith"
148
+ """
149
+ return name.replace(" ", "_").lower()
150
+
151
+
152
+ def find_available_port(
153
+ start_port: int = 3000, max_port: int = 3100, host: str = "localhost"
154
+ ) -> int:
155
+ """
156
+ Find a TCP port with no listener on `host`, starting from start_port.
157
+ """
158
+ import socket
159
+
160
+ def is_port_in_use(
161
+ port: int, host: str = "localhost", timeout: float = 0.2
162
+ ) -> bool:
163
+ """
164
+ Return True if *any* TCP listener is active on the given host:port.
165
+ This works across IPv4/IPv6 by using getaddrinfo().
166
+ """
167
+ try:
168
+ for family, socktype, proto, _canonname, sockaddr in socket.getaddrinfo(
169
+ host, port, type=socket.SOCK_STREAM
170
+ ):
171
+ with socket.socket(family, socktype, proto) as s:
172
+ s.settimeout(timeout)
173
+ if s.connect_ex(sockaddr) == 0:
174
+ return True
175
+ except socket.gaierror:
176
+ # host couldn't be resolved; treat as no listener
177
+ return False
178
+ return False
179
+
180
+ for port in range(start_port, max_port + 1):
181
+ if not is_port_in_use(port, host=host):
182
+ return port
183
+ raise RuntimeError(
184
+ f"No free ports available in range {start_port}-{max_port} for host={host!r}"
185
+ )
186
+
187
+
188
+ def has_content_for_myst(content_path: Path) -> bool:
189
+ """Check if a content directory has any content files"""
190
+ return any(
191
+ content_path.glob("*.md")
192
+ or content_path.glob("*.ipynb")
193
+ or content_path.glob("*.tex")
194
+ )
195
+
196
+
197
+ def handle_passthrough_help(
198
+ ctx: click.Context,
199
+ underlying_command: list[str],
200
+ show_underlying: bool = True,
201
+ help_flags: tuple[str, ...] = ("--help", "-h"),
202
+ ) -> None:
203
+ """Handle --help for commands that pass through to underlying tools.
204
+
205
+ Shows both Click's help (custom options) and the underlying tool's help.
206
+
207
+ Args:
208
+ ctx: Click context
209
+ underlying_command: Command to show help for (e.g., ["cz", "bump"])
210
+ show_underlying: Whether to show underlying tool's help (default: True)
211
+ help_flags: Flags that trigger help display (default: ("--help", "-h"))
212
+ Use ("--help",) if -h is reserved by the underlying tool
213
+ """
214
+ # Check if any help flag is present
215
+ if any(flag in ctx.args for flag in help_flags):
216
+ # Show Click's help first (our custom options)
217
+ click.echo(ctx.get_help())
218
+
219
+ # Then show underlying tool's help if requested
220
+ if show_underlying:
221
+ click.echo("\n" + "=" * 60)
222
+ click.echo(f"Additional options from '{' '.join(underlying_command)}':")
223
+ click.echo("=" * 60 + "\n")
224
+ subprocess.run([*underlying_command, "--help"])
225
+
226
+ ctx.exit(0)
227
+
228
+
229
+ def normalize_static_path(path: str) -> str:
230
+ """
231
+ Normalize paths for static assets.
232
+
233
+ Only allows two formats:
234
+ 1. Pure filename/path without ./ or ../ (e.g., "logo.svg", "somewhere/logo.svg")
235
+ 2. Absolute path starting with / (e.g., "/somewhere/logo.svg")
236
+
237
+ Args:
238
+ path: User-provided path from afterpython.toml
239
+
240
+ Returns:
241
+ Normalized path
242
+
243
+ Raises:
244
+ ValueError: If path contains ./ or ../ or other invalid patterns
245
+
246
+ Examples:
247
+ >>> normalize_static_path("logo.svg")
248
+ 'logo.svg'
249
+ >>> normalize_static_path("somewhere/logo.svg")
250
+ '/somewhere/logo.svg'
251
+ >>> normalize_static_path("/somewhere/logo.svg")
252
+ '/somewhere/logo.svg'
253
+ >>> normalize_static_path("./logo.svg") # Raises ValueError
254
+ >>> normalize_static_path("../logo.svg") # Raises ValueError
255
+ >>> normalize_static_path("static/logo.svg") # Raises ValueError
256
+ """
257
+ if not path:
258
+ return ""
259
+
260
+ # Reject paths with ./ or ../
261
+ if "./" in path or "../" in path or path.startswith(".") or path.startswith(".."):
262
+ raise ValueError(
263
+ f"Invalid path '{path}': paths cannot contain './' or '../'. "
264
+ f"Use either a pure filename (e.g., 'logo.svg') or absolute path (e.g., '/somewhere/logo.svg')"
265
+ )
266
+
267
+ # Reject paths with 'static' in them
268
+ if "static" in path.lower():
269
+ raise ValueError(
270
+ f"Invalid path '{path}': do not include 'static' in the path. "
271
+ f"Files are assumed to be in the static/ directory."
272
+ )
273
+
274
+ # If it starts with /, it's absolute - keep as is
275
+ if not path.startswith("/"):
276
+ return "/" + path
277
+ else:
278
+ return path
279
+
280
+
281
+ def get_relative_static_prefix(file_path: Path, content_type_path: Path) -> str:
282
+ """Calculate the relative path prefix from a file to its content type's static/ folder.
283
+
284
+ Args:
285
+ file_path: Path to the content file (e.g., blog/layer/blog5.ipynb)
286
+ content_type_path: Path to the content type folder (e.g., blog/)
287
+
288
+ Returns:
289
+ Relative prefix to reach static/ (e.g., "./" or "../" or "../../")
290
+ """
291
+ # Get the depth: how many directories deep the file is from content_type_path
292
+ relative = file_path.parent.relative_to(content_type_path)
293
+ depth = len(relative.parts)
294
+
295
+ # Build the correct prefix
296
+ if depth == 0:
297
+ return "./" # File is directly in content_type_path
298
+ else:
299
+ return "../" * depth
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,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: afterpython
3
+ Version: 0.1.0
4
+ Summary: CLI tool to streamline Python package maintenance and generate a dedicated project website
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: Typing :: Typed
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
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: python-dotenv>=1.2.1
24
+ Requires-Dist: pyproject-metadata>=0.10.0
25
+ Requires-Dist: mystmd>=1.7.0
26
+ Requires-Dist: pdoc>=16.0.0
27
+ Requires-Dist: ruff>=0.14.8
28
+ Requires-Dist: pre-commit>=4.5.0
29
+ Requires-Dist: commitizen>=4.10.0
30
+ Requires-Python: >=3.11
31
+ Project-URL: documentation, https://afterpython.afterpython.org/doc
32
+ Project-URL: homepage, https://afterpython.afterpython.org
33
+ Project-URL: repository, https://github.com/AfterPythonOrg/afterpython
34
+ Description-Content-Type: text/markdown
35
+
36
+ # AfterPython: Python Package Maintenance Toolkit and Project Website Generator
37
+
38
+ [![afterpython](https://afterpython.org/shield.svg)](https://afterpython.org)
39
+ ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython)
40
+ [![PyPI](https://img.shields.io/pypi/v/afterpython.svg)](https://pypi.org/project/afterpython)
41
+ ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython)
42
+ [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
43
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/AfterPythonOrg/afterpython)
44
+ <!-- [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh) -->
45
+
46
+
47
+ [MyST]: https://mystmd.org
48
+ [MyST Markdown]: https://mystmd.org/spec/
49
+ [Jupyter Notebook]: https://jupyter.org
50
+ [pre-commit]: https://pre-commit.com
51
+ [pagefind]: https://pagefind.app
52
+ [SemVer]: https://semver.org
53
+ [pdoc]: https://pdoc.dev/
54
+ [GitHub Actions]: https://github.com/features/actions
55
+ [PyTorch]: https://pytorch.org
56
+ [Scikit-Learn]: https://scikit-learn.org
57
+ [WebLLM]: https://webllm.mlc.ai/
58
+ [project-website-template]: https://github.com/AfterPythonOrg/project-website-template
59
+ [commitizen]: https://github.com/commitizen-tools/commitizen
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
+ - [x] Zero-config orchestration — Pre-configured modern tooling with sane defaults (see [Tech Stack](#tech-stack)), so you can start maintaining packages immediately **without learning each tool**
81
+ - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
82
+ - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
83
+ - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
84
+
85
+ ---
86
+ ## Project Website
87
+ > The project website for `afterpython` is created using `afterpython` itself. See the [**website**](https://afterpython.afterpython.org).
88
+ <picture>
89
+ <source media="(prefers-color-scheme: dark)" srcset="afterpython/static/website-dark.png">
90
+ <source media="(prefers-color-scheme: light)" srcset="afterpython/static/website-light.png">
91
+ <img alt="Project Website" src="afterpython/static/website-light.png">
92
+ </picture>
93
+
94
+ You can create your own website too and deploy it to GitHub Pages in **less than a minute**! See [Quickstart](https://afterpython.afterpython.org/doc/quickstart).
95
+
96
+ ---
97
+ ## Installation
98
+ ```bash
99
+ # install afterpython as a dev dependency
100
+ uv add --dev afterpython
101
+
102
+ # initialize afterpython
103
+ ap init
104
+ ```
105
+
106
+
107
+ ---
108
+ ## CLI Commands
109
+ ```bash
110
+ # show all commands
111
+ ap --help
112
+
113
+ # or use terminal UI (TUI)
114
+ ap tui
115
+ ```
116
+
117
+
118
+ ---
119
+ ## Tech Stack
120
+ - [MyST]
121
+ - [project-website-template]
122
+ - [pre-commit]
123
+ - [GitHub Actions]
124
+ - [pdoc]
125
+ - [commitizen]
126
+ - [uv]
127
+ - [ruff]
128
+ - [pagefind]
129
+ - [WebLLM]
130
+ <!-- - ty -->
131
+ <!-- - [pixi] -->
@@ -0,0 +1,57 @@
1
+ afterpython/__init__.py,sha256=aNochs72gU2NR1cBzIkk3NxSsdJ7ceHluI5gdYwaFpI,289
2
+ afterpython/_io/toml.py,sha256=dvmq6n4MhlJ02u_YjfC3mOnCWatfCn_J1lugavigoJw,3410
3
+ afterpython/_io/yaml.py,sha256=raksve_gaIFJim8lQrq-kQHkRjTloed7CT2FY0oUJSg,654
4
+ afterpython/_paths.py,sha256=FTwQ7necKYuRosCyMZPfv1eqaEmGIsyqd3yXpm-WQTA,1584
5
+ afterpython/_typing.py,sha256=FbphSGoujPuTfCZqt0HaKQrBgzhRpCOEAEQU6Cbl7oA,145
6
+ afterpython/builders/__init__.py,sha256=YMfYfsbSJNMjqfq9GRsQj8ayWUhtZKpvrc0oFUASICo,575
7
+ afterpython/builders/content_json.py,sha256=-LTkeA8oA6XEudc2b1H7U0-UwcJlHn7xgICeweMYrG0,4805
8
+ afterpython/builders/index_md.py,sha256=SDVHnVftL4D1AA8jw2YvRdTem_Ho8v8DcrORVjVtW8A,3762
9
+ afterpython/builders/jupyter_notebook.py,sha256=2H5Qv_IIkmOud-GYZPYjB0E30fVZubpUUQ5gFrtGSqU,10140
10
+ afterpython/builders/markdown.py,sha256=0ExfRqvOTWsgUkAVaGGNkiNDtNedRr7JR1yTGxpbsmA,8736
11
+ afterpython/builders/metadata.py,sha256=--0zWaI0ZI1wKeezcyqSUy-h1IuLqnI33ateoAHIaBU,1337
12
+ afterpython/cli/__init__.py,sha256=G4AB-rllhmr7tURFc5cXJfkjZZ2bXtMBg3oy4zUGEFw,84
13
+ afterpython/cli/commands/add.py,sha256=Ppp4msdvOwN9_-fdG5-b0-IV9p0Eef9CiIEU3srUjYk,1792
14
+ afterpython/cli/commands/build.py,sha256=Vvyd7AkL2Tw3vEDhi4bEOWD9hdtxon7SgAzXUeh5D8w,9050
15
+ afterpython/cli/commands/bump.py,sha256=2MZBQ1kxjr0bWhA--_xHwdwjsUlCyrXPn3pJ5NDZO4k,4088
16
+ afterpython/cli/commands/check.py,sha256=lrPaVThAwxxnczu-VOZT7dd7aKjstnMQfX9UpZY3Lxs,1058
17
+ afterpython/cli/commands/clean.py,sha256=havP43ZC1AKFzvscsRMF_to6TvOwruxGA5qzr6dMHvg,2529
18
+ afterpython/cli/commands/commit.py,sha256=9QOKyt4tfTJzZEdHEPAdpUc4stYMSvG3-31uADuBxn4,1420
19
+ afterpython/cli/commands/commitizen.py,sha256=H3jewz4VkZi5FYTWw3USVM3XosRx12kPShOQGxD3LNM,842
20
+ afterpython/cli/commands/dev.py,sha256=YIJECfroGRQnuMWWRC4W_DMA0eJoQssufj8WBDjQVOM,6796
21
+ afterpython/cli/commands/format.py,sha256=VFwCG9KDTF6j4Y_Ewp4_RpS8uNBQNOAj9HU_G97my5A,710
22
+ afterpython/cli/commands/init.py,sha256=UiE8QIhDV9NqoqU2EU_FMAlJ2Y18lFF0x2JmcyiAQxc,3044
23
+ afterpython/cli/commands/init_branch_rules.py,sha256=fzAm8D5qbW3u4bEMpqPuRgO-0akuCGG7pu09JlKhYn4,243
24
+ afterpython/cli/commands/install.py,sha256=B8JuQmtU_4EZpHct2J5j30BGLkGmvwKvU26upbmabvQ,750
25
+ afterpython/cli/commands/lock.py,sha256=Kppu_0tnqdbP6Gbulxf123f-cFzTGIw39si2O7rDo3Q,603
26
+ afterpython/cli/commands/pre_commit.py,sha256=QIuzTZEfw4uPMqmQT8YmyD0yewBcwjLvcnbVjRbonQE,1978
27
+ afterpython/cli/commands/preview.py,sha256=sBwe46H0V-U7I4d0-u-nXmPBWGmkjKhkUTbqV927zRM,593
28
+ afterpython/cli/commands/release.py,sha256=6SDEnPtuCL0_XUqWUlvRSKxAv011QqOD0kc5pKu7378,2479
29
+ afterpython/cli/commands/remove.py,sha256=Fl79JSI9IdHyzYflocXxcxgBkBPkNFAW98Yuijly26g,1812
30
+ afterpython/cli/commands/start.py,sha256=q4rUkIllOO8wJQ9ABtsx-dqkW1yC0vltAtSkiHN_GH4,4002
31
+ afterpython/cli/commands/sync.py,sha256=u9L3aq04M5VHh0FByDSpmBD7F1RYCNsIgvZFifnRZ6A,6042
32
+ afterpython/cli/commands/update.py,sha256=4h_zKp72e4RmXUKHyeP6kfHqr4V3IugSOnxa-E1F55I,6312
33
+ afterpython/cli/main.py,sha256=m3BJ5rOlllUyOngD2HF2fosDLrduREzAQKpsTqcYPZc,4956
34
+ afterpython/const.py,sha256=B_yUBmHPyu98aOibwt7idWbhKNBBed0TeYXNBd8tPTk,157
35
+ afterpython/main.py,sha256=w0z6mTib3u4p2HxMrk-tIJlZtur7uRaqsbE0ewlYlSs,490
36
+ afterpython/pcu.py,sha256=EoEhSzFZ49b5qc9KH2RLxIHSzY8YFM5uhtw07chcrr8,6093
37
+ afterpython/templates/ci-workflow-template.yml,sha256=w0BiV9F_y9YNTd6mTD7xRlJgwRQw61AinhiACXlEQqk,5933
38
+ afterpython/templates/cz-template.toml,sha256=2qwbQLb0nbZaA63oWXFFzvzPNBsFZHATL1C2rdNdjjs,185
39
+ afterpython/templates/dependabot-template.yml,sha256=CBQEgh8EsXw2X7IqzGtFI7lVT9liF6PW4DvGLB0X2LY,1136
40
+ afterpython/templates/deploy-workflow-template.yml,sha256=2W4K3lKMYjSTPrlXIsMweBzqP9IoLtZXAAcUu5FA5NA,2365
41
+ afterpython/templates/pre-commit-config-template.yaml,sha256=u_JF4YHGHIWxLdcX5BS__VsVgOwwVQQsSZGPNR2Yyf0,915
42
+ afterpython/templates/release-workflow-template.yml,sha256=3YBkgrqmi--uznslpMxPz7Vj7L4_NPlhDxgwYBt-Ui8,1606
43
+ afterpython/templates/ruff-template.toml,sha256=lNCd4EthoLScr4cyVEgGUH4xK5CfIX3f9I3zkJHAYgg,1008
44
+ afterpython/tools/_afterpython.py,sha256=G1075u8VT6s-TjEcGajqEYS3pZoW0sGyJM7DJohMOPE,4244
45
+ afterpython/tools/_git.py,sha256=aO-qDEQ4OevD_B74DYKK4nQKYeiaS1BXmYc8paJRfbQ,3772
46
+ afterpython/tools/branch_rules.py,sha256=ciQ2H2YBnoCZu_plyVeIHzf2FcAILRFhpjzOR2_IfJg,3159
47
+ afterpython/tools/commitizen.py,sha256=ZO1qHmzUU0myd85tncv76WtIR7DB_AoQGSUvaf78Ohg,438
48
+ afterpython/tools/github_actions.py,sha256=zVmg9aqUg-EO0CnbRHl9i8TmFtXpt7nd0VjzckqqE30,1527
49
+ afterpython/tools/myst.py,sha256=2VO-gD25PbGSD8-v1sLI1v3Q9loahRd3ednKrW4_Utc,8113
50
+ afterpython/tools/pre_commit.py,sha256=oNsrUkPI13rSSBSyhyTTKPnQGhDxLIPYhxu6kL0fDmo,1206
51
+ afterpython/tools/pyproject.py,sha256=kb7BDn24yH_xS2F5_L0dK-GL4KgOJS-MIIUQhHnLorI,3507
52
+ afterpython/utils.py,sha256=s1FIJP6FvpAcK8SujB8glFbiKx-ZCltywh5ihaOhL0U,9516
53
+ afterpython/version.py,sha256=hihRV1S-gc1m46OaXD32ikkEkqzs-Gtq7AcAqOQV-wY,1603
54
+ afterpython-0.1.0.dist-info/WHEEL,sha256=93kfTGt3a0Dykt_T-gsjtyS5_p8F_d6CE1NwmBOirzo,79
55
+ afterpython-0.1.0.dist-info/entry_points.txt,sha256=roa0PDgBueM1JPbRG0G2OSxzaN394OUL_oS9iEwVLBc,80
56
+ afterpython-0.1.0.dist-info/METADATA,sha256=pld74AzH2JQEKTgHbrVpJ0BJUNk2k7HAzumazs-83bo,5779
57
+ afterpython-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.16
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ ap = afterpython.main:run_cli
3
+ pcu = afterpython.main:run_pcu
4
+