afterpython 0.0.1.dev1__tar.gz → 0.1.0__tar.gz

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 (65) hide show
  1. afterpython-0.1.0/PKG-INFO +131 -0
  2. afterpython-0.1.0/README.md +96 -0
  3. afterpython-0.1.0/pyproject.toml +73 -0
  4. afterpython-0.1.0/src/afterpython/__init__.py +13 -0
  5. afterpython-0.1.0/src/afterpython/_io/toml.py +105 -0
  6. afterpython-0.1.0/src/afterpython/_io/yaml.py +26 -0
  7. afterpython-0.1.0/src/afterpython/_paths.py +39 -0
  8. afterpython-0.1.0/src/afterpython/_typing.py +4 -0
  9. afterpython-0.1.0/src/afterpython/builders/__init__.py +17 -0
  10. afterpython-0.1.0/src/afterpython/builders/content_json.py +131 -0
  11. afterpython-0.1.0/src/afterpython/builders/index_md.py +94 -0
  12. afterpython-0.1.0/src/afterpython/builders/jupyter_notebook.py +293 -0
  13. afterpython-0.1.0/src/afterpython/builders/markdown.py +271 -0
  14. afterpython-0.1.0/src/afterpython/builders/metadata.py +50 -0
  15. afterpython-0.1.0/src/afterpython/cli/__init__.py +3 -0
  16. afterpython-0.1.0/src/afterpython/cli/commands/add.py +67 -0
  17. afterpython-0.1.0/src/afterpython/cli/commands/build.py +260 -0
  18. afterpython-0.1.0/src/afterpython/cli/commands/bump.py +118 -0
  19. afterpython-0.1.0/src/afterpython/cli/commands/check.py +36 -0
  20. afterpython-0.1.0/src/afterpython/cli/commands/clean.py +85 -0
  21. afterpython-0.1.0/src/afterpython/cli/commands/commit.py +52 -0
  22. afterpython-0.1.0/src/afterpython/cli/commands/commitizen.py +32 -0
  23. afterpython-0.1.0/src/afterpython/cli/commands/dev.py +213 -0
  24. afterpython-0.1.0/src/afterpython/cli/commands/format.py +28 -0
  25. afterpython-0.1.0/src/afterpython/cli/commands/init.py +107 -0
  26. afterpython-0.1.0/src/afterpython/cli/commands/init_branch_rules.py +9 -0
  27. afterpython-0.1.0/src/afterpython/cli/commands/install.py +28 -0
  28. afterpython-0.1.0/src/afterpython/cli/commands/lock.py +23 -0
  29. afterpython-0.1.0/src/afterpython/cli/commands/pre_commit.py +66 -0
  30. afterpython-0.1.0/src/afterpython/cli/commands/preview.py +22 -0
  31. afterpython-0.1.0/src/afterpython/cli/commands/release.py +67 -0
  32. afterpython-0.1.0/src/afterpython/cli/commands/remove.py +67 -0
  33. afterpython-0.1.0/src/afterpython/cli/commands/start.py +144 -0
  34. afterpython-0.1.0/src/afterpython/cli/commands/sync.py +152 -0
  35. afterpython-0.1.0/src/afterpython/cli/commands/update.py +176 -0
  36. afterpython-0.1.0/src/afterpython/cli/main.py +132 -0
  37. afterpython-0.1.0/src/afterpython/const.py +4 -0
  38. afterpython-0.1.0/src/afterpython/main.py +25 -0
  39. afterpython-0.1.0/src/afterpython/pcu.py +166 -0
  40. afterpython-0.1.0/src/afterpython/templates/ci-workflow-template.yml +197 -0
  41. afterpython-0.1.0/src/afterpython/templates/cz-template.toml +7 -0
  42. afterpython-0.1.0/src/afterpython/templates/dependabot-template.yml +30 -0
  43. afterpython-0.1.0/src/afterpython/templates/deploy-workflow-template.yml +88 -0
  44. afterpython-0.1.0/src/afterpython/templates/pre-commit-config-template.yaml +48 -0
  45. afterpython-0.1.0/src/afterpython/templates/release-workflow-template.yml +56 -0
  46. afterpython-0.1.0/src/afterpython/templates/ruff-template.toml +32 -0
  47. afterpython-0.1.0/src/afterpython/tools/_afterpython.py +128 -0
  48. afterpython-0.1.0/src/afterpython/tools/_git.py +137 -0
  49. afterpython-0.1.0/src/afterpython/tools/branch_rules.py +106 -0
  50. afterpython-0.1.0/src/afterpython/tools/commitizen.py +13 -0
  51. afterpython-0.1.0/src/afterpython/tools/github_actions.py +48 -0
  52. afterpython-0.1.0/src/afterpython/tools/myst.py +208 -0
  53. afterpython-0.1.0/src/afterpython/tools/pre_commit.py +37 -0
  54. afterpython-0.1.0/src/afterpython/tools/pyproject.py +110 -0
  55. afterpython-0.1.0/src/afterpython/utils.py +299 -0
  56. afterpython-0.1.0/src/afterpython/version.py +39 -0
  57. afterpython-0.0.1.dev1/LICENSE +0 -201
  58. afterpython-0.0.1.dev1/PKG-INFO +0 -17
  59. afterpython-0.0.1.dev1/README.md +0 -1
  60. afterpython-0.0.1.dev1/afterpython/images/favicon.ico +0 -0
  61. afterpython-0.0.1.dev1/afterpython/images/logo-trace.svg +0 -3
  62. afterpython-0.0.1.dev1/afterpython/images/logo.png +0 -0
  63. afterpython-0.0.1.dev1/afterpython/images/logo.svg +0 -2
  64. afterpython-0.0.1.dev1/afterpython/main.py +0 -1
  65. afterpython-0.0.1.dev1/pyproject.toml +0 -13
@@ -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,96 @@
1
+ # AfterPython: Python Package Maintenance Toolkit and Project Website Generator
2
+
3
+ [![afterpython](https://afterpython.org/shield.svg)](https://afterpython.org)
4
+ ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython)
5
+ [![PyPI](https://img.shields.io/pypi/v/afterpython.svg)](https://pypi.org/project/afterpython)
6
+ ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython)
7
+ [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
8
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/AfterPythonOrg/afterpython)
9
+ <!-- [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh) -->
10
+
11
+
12
+ [MyST]: https://mystmd.org
13
+ [MyST Markdown]: https://mystmd.org/spec/
14
+ [Jupyter Notebook]: https://jupyter.org
15
+ [pre-commit]: https://pre-commit.com
16
+ [pagefind]: https://pagefind.app
17
+ [SemVer]: https://semver.org
18
+ [pdoc]: https://pdoc.dev/
19
+ [GitHub Actions]: https://github.com/features/actions
20
+ [PyTorch]: https://pytorch.org
21
+ [Scikit-Learn]: https://scikit-learn.org
22
+ [WebLLM]: https://webllm.mlc.ai/
23
+ [project-website-template]: https://github.com/AfterPythonOrg/project-website-template
24
+ [commitizen]: https://github.com/commitizen-tools/commitizen
25
+ [uv]: https://docs.astral.sh/uv/
26
+ [ruff]: https://docs.astral.sh/ruff/
27
+
28
+
29
+ ## Problem
30
+ Going from **writing Python code to publishing and maintaining a package** is **time-consuming**.
31
+ 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.
32
+
33
+
34
+ ## Solution
35
+ `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.
36
+
37
+ ---
38
+ `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.
39
+
40
+
41
+ ## Core Features
42
+ - [x] Write content directly in [MyST Markdown] or [Jupyter Notebook]
43
+ - [x] Go from writing to **website deployment in minutes** — no need to learn any of the underlying tools
44
+ - [x] Centralize all your content in a modern, **unified project website** — from documentation to blog posts
45
+ - [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**
46
+ - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
47
+ - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
48
+ - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
49
+
50
+ ---
51
+ ## Project Website
52
+ > The project website for `afterpython` is created using `afterpython` itself. See the [**website**](https://afterpython.afterpython.org).
53
+ <picture>
54
+ <source media="(prefers-color-scheme: dark)" srcset="afterpython/static/website-dark.png">
55
+ <source media="(prefers-color-scheme: light)" srcset="afterpython/static/website-light.png">
56
+ <img alt="Project Website" src="afterpython/static/website-light.png">
57
+ </picture>
58
+
59
+ 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).
60
+
61
+ ---
62
+ ## Installation
63
+ ```bash
64
+ # install afterpython as a dev dependency
65
+ uv add --dev afterpython
66
+
67
+ # initialize afterpython
68
+ ap init
69
+ ```
70
+
71
+
72
+ ---
73
+ ## CLI Commands
74
+ ```bash
75
+ # show all commands
76
+ ap --help
77
+
78
+ # or use terminal UI (TUI)
79
+ ap tui
80
+ ```
81
+
82
+
83
+ ---
84
+ ## Tech Stack
85
+ - [MyST]
86
+ - [project-website-template]
87
+ - [pre-commit]
88
+ - [GitHub Actions]
89
+ - [pdoc]
90
+ - [commitizen]
91
+ - [uv]
92
+ - [ruff]
93
+ - [pagefind]
94
+ - [WebLLM]
95
+ <!-- - ty -->
96
+ <!-- - [pixi] -->
@@ -0,0 +1,73 @@
1
+ [project]
2
+ name = "afterpython"
3
+ version = "0.1.0"
4
+ description = "CLI tool to streamline Python package maintenance and generate a dedicated project website"
5
+ license = "Apache-2.0"
6
+ authors = [
7
+ { name = "Stephen Yau", email = "softwareentrepreneer+afterpython@gmail.com" },
8
+ ]
9
+ readme = "README.md"
10
+ keywords = [
11
+ "package-maintenance",
12
+ "project-website",
13
+ "static-site-generator",
14
+ "developer-tools",
15
+ "documentation",
16
+ "cli-tool",
17
+ "ci/cd",
18
+ "mystmd",
19
+ "semantic-release",
20
+ "python-packaging",
21
+ ]
22
+ classifiers = [
23
+ "Operating System :: OS Independent",
24
+ "Intended Audience :: Developers",
25
+ "Topic :: Software Development :: Build Tools",
26
+ "Topic :: Software Development :: Documentation",
27
+ "Typing :: Typed",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ # "Programming Language :: Python :: 3.14",
32
+ ]
33
+ requires-python = ">=3.11"
34
+ dependencies = [
35
+ "click>=8.3.1",
36
+ "trogon>=0.6.0",
37
+ "httpx>=0.28.1",
38
+ "tomlkit>=0.13.3",
39
+ "ruamel-yaml>=0.18.16",
40
+ "gitpython>=3.1.45",
41
+ # "pygithub>=2.8.1",
42
+ "python-dotenv>=1.2.1",
43
+ "pyproject-metadata>=0.10.0",
44
+ # "jupytext>=1.18.1",
45
+ # "jinja2>=3.1.6",
46
+ "mystmd>=1.7.0",
47
+ "pdoc>=16.0.0",
48
+ "ruff>=0.14.8",
49
+ "pre-commit>=4.5.0",
50
+ "commitizen>=4.10.0",
51
+ # "python-semantic-release>=10.5.2",
52
+ ]
53
+
54
+ [project.urls]
55
+ homepage = "https://afterpython.afterpython.org"
56
+ repository = "https://github.com/AfterPythonOrg/afterpython"
57
+ documentation = "https://afterpython.afterpython.org/doc"
58
+
59
+ [project.scripts]
60
+ ap = "afterpython.main:run_cli"
61
+ pcu = "afterpython.main:run_pcu"
62
+
63
+ [tool.uv]
64
+ # override the restrictive version constraints "platformdirs~=4.2.2", "nodeenv~=1.9.1" in package `mystmd`
65
+ override-dependencies = ["platformdirs>=4.5.0", "nodeenv>=1.9.1"]
66
+
67
+ [build-system]
68
+ requires = ["uv_build>=0.9.16,<0.10.0"]
69
+ build-backend = "uv_build"
70
+
71
+ [tool.uv.build-backend]
72
+ module-name = "afterpython"
73
+ module-root = "src"
@@ -0,0 +1,13 @@
1
+ # import builtins
2
+ # from pprint import pprint
3
+ from importlib.metadata import version
4
+
5
+ from afterpython._paths import Paths
6
+
7
+ # make pprint available in the global namespace
8
+ # builtins.pprint = pprint
9
+
10
+ paths = Paths()
11
+
12
+ __version__ = version("afterpython")
13
+ __all__ = ("__version__", "paths")
@@ -0,0 +1,105 @@
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 tomlkit.toml_document import TOMLDocument
9
+
10
+ import tomlkit
11
+
12
+
13
+ # VIBE-CODED
14
+ def _from_tomlkit(value):
15
+ """Recursively convert tomlkit objects to plain Python data structures.
16
+
17
+ This ensures compatibility with other libraries (like ruamel.yaml):
18
+ - tomlkit containers → plain Python containers
19
+ - tomlkit primitives → plain Python primitives
20
+ """
21
+ from tomlkit.items import AoT, Array, InlineTable, Table
22
+
23
+ if value is None:
24
+ return None
25
+
26
+ # Check containers first (these are the problematic ones)
27
+ if isinstance(value, (Table, InlineTable)):
28
+ # tomlkit dict-like → plain dict
29
+ return {k: _from_tomlkit(v) for k, v in value.items()}
30
+ elif isinstance(value, (Array, AoT)):
31
+ # tomlkit array → plain list
32
+ return [_from_tomlkit(item) for item in value]
33
+ elif isinstance(value, dict):
34
+ # Already a dict, but recurse to handle nested tomlkit objects
35
+ return {k: _from_tomlkit(v) for k, v in value.items()}
36
+ elif isinstance(value, list):
37
+ # Already a list, but recurse to handle nested tomlkit objects
38
+ return [_from_tomlkit(item) for item in value]
39
+
40
+ # Primitives - tomlkit primitives inherit from Python types
41
+ # Just convert explicitly to be safe
42
+ elif isinstance(value, bool): # MUST check bool before int!
43
+ return bool(value)
44
+ elif isinstance(value, int):
45
+ return int(value)
46
+ elif isinstance(value, float):
47
+ return float(value)
48
+ elif isinstance(value, str):
49
+ return str(value)
50
+ else:
51
+ # datetime, date, time, or already plain Python type
52
+ return value
53
+
54
+
55
+ # VIBE-CODED
56
+ def _to_tomlkit(value):
57
+ """Recursively convert Python data structures to tomlkit objects.
58
+
59
+ This ensures proper TOML formatting:
60
+ - List of dicts → array of inline tables: [{key = val}]
61
+ - Nested dicts → nested tables with proper structure
62
+ """
63
+ from tomlkit import array, inline_table
64
+
65
+ if value is None:
66
+ return None
67
+
68
+ if isinstance(value, dict):
69
+ # Plain dict → recursively convert values
70
+ return {k: _to_tomlkit(v) for k, v in value.items()}
71
+ elif isinstance(value, list):
72
+ if not value:
73
+ # Empty list → empty tomlkit array
74
+ return array()
75
+
76
+ # Check if ALL items are dicts (should be inline tables)
77
+ if all(isinstance(item, dict) for item in value):
78
+ arr = array()
79
+ for item in value:
80
+ tbl = inline_table()
81
+ # Recursively convert the dict values
82
+ tbl.update({k: _to_tomlkit(v) for k, v in item.items()})
83
+ arr.append(tbl)
84
+ return arr
85
+ else:
86
+ # Mixed or non-dict items → regular array
87
+ arr = array()
88
+ for item in value:
89
+ arr.append(_to_tomlkit(item))
90
+ return arr
91
+ else:
92
+ # Primitives (str, int, float, bool, datetime, etc.)
93
+ # tomlkit handles these natively
94
+ return value
95
+
96
+
97
+ def read_toml(file_path: Path) -> TOMLDocument:
98
+ with open(file_path, "rb") as f:
99
+ data: TOMLDocument = tomlkit.parse(f.read())
100
+ return data
101
+
102
+
103
+ def write_toml(file_path: Path, data: TOMLDocument | dict):
104
+ with open(file_path, "w") as f:
105
+ f.write(tomlkit.dumps(data))
@@ -0,0 +1,26 @@
1
+ from pathlib import Path
2
+
3
+ from ruamel.yaml import YAML
4
+ from ruamel.yaml.comments import CommentedMap
5
+
6
+
7
+ def _get_yaml() -> YAML:
8
+ """Get configured YAML instance that preserves order, comments, and formatting"""
9
+ yaml = YAML()
10
+ yaml.preserve_quotes = True
11
+ yaml.default_flow_style = False
12
+ yaml.width = 4096 # Prevent line wrapping
13
+ return yaml
14
+
15
+
16
+ def read_yaml(file_path: Path) -> CommentedMap:
17
+ yaml = _get_yaml()
18
+ with open(file_path) as f:
19
+ return yaml.load(f)
20
+
21
+
22
+ def write_yaml(file_path: Path, data: dict):
23
+ """Write YAML data"""
24
+ yaml = _get_yaml()
25
+ with open(file_path, "w") as f:
26
+ yaml.dump(data, f)
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+
4
+
5
+ @dataclass
6
+ class Paths:
7
+ package_path: Path = field(init=False)
8
+ templates_path: Path = field(init=False)
9
+ user_path: Path = field(init=False)
10
+ pyproject_path: Path = field(init=False)
11
+ afterpython_path: Path = field(init=False)
12
+ website_path: Path = field(init=False)
13
+ build_path: Path = field(init=False)
14
+ static_path: Path = field(init=False)
15
+
16
+ def __post_init__(self):
17
+ # package path is the path to the afterpython package in user's site-packages
18
+ self.package_path = Path(__file__).resolve().parents[0]
19
+ self.templates_path = self.package_path / "templates"
20
+ self.user_path = self._find_project_root()
21
+ self.pyproject_path = self.user_path / "pyproject.toml"
22
+ self.afterpython_path = self.user_path / "afterpython"
23
+ self.website_path = self.afterpython_path / "_website"
24
+ self.build_path = self.afterpython_path / "_build"
25
+ self.static_path = self.afterpython_path / "static"
26
+
27
+ def _find_project_root(self) -> Path:
28
+ """Find the project root by looking for pyproject.toml in current or parent directories."""
29
+ current = Path.cwd()
30
+
31
+ # Check current directory and all parents
32
+ for path in [current, *current.parents]:
33
+ if (path / "pyproject.toml").exists():
34
+ return path
35
+
36
+ # If no pyproject.toml found, raise an error like uv does
37
+ raise FileNotFoundError(
38
+ "No pyproject.toml found in current directory or any parent directory"
39
+ )
@@ -0,0 +1,4 @@
1
+ from typing import Literal, TypeAlias
2
+
3
+ NodeEnv: TypeAlias = dict[str, str]
4
+ tContentType = Literal["doc", "blog", "tutorial", "example", "guide"]
@@ -0,0 +1,17 @@
1
+ from afterpython.builders.content_json import build_content_json
2
+ from afterpython.builders.index_md import (
3
+ create_placeholder_index_md_files,
4
+ delete_placeholder_index_md_files,
5
+ )
6
+ from afterpython.builders.jupyter_notebook import build_jupyter_notebooks
7
+ from afterpython.builders.markdown import build_markdown
8
+ from afterpython.builders.metadata import build_metadata
9
+
10
+ __all__ = (
11
+ "build_content_json",
12
+ "build_jupyter_notebooks",
13
+ "build_markdown",
14
+ "build_metadata",
15
+ "create_placeholder_index_md_files",
16
+ "delete_placeholder_index_md_files",
17
+ )
@@ -0,0 +1,131 @@
1
+ """
2
+ Build {content_type}.json for non-doc content type (blog, tutorial, example, guide)
3
+
4
+ Combines all individual content JSON files from MyST build output into a single
5
+ manifest file for SvelteKit to consume (e.g., blog.json, tutorial.json).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from afterpython._typing import tContentType
14
+
15
+ import json
16
+ from pathlib import Path
17
+
18
+ import click
19
+
20
+ import afterpython as ap
21
+ from afterpython.const import CONTENT_TYPES
22
+
23
+
24
+ def standardize_data(content_type: tContentType, content: dict) -> dict:
25
+ """
26
+ Standardize the content dictionary to a single level.
27
+ """
28
+ from afterpython.tools._afterpython import read_afterpython
29
+
30
+ afterpython = read_afterpython()
31
+ website = afterpython.get("website", {})
32
+ content_type_config = website.get(content_type, {})
33
+ featured_post = content_type_config.get("featured_post", "")
34
+
35
+ frontmatter = content.get("frontmatter", {})
36
+ # e.g. featured_post = "blog4.md" or "blog4"
37
+ is_featured = content["slug"] == featured_post or content["location"].endswith(
38
+ featured_post
39
+ )
40
+ return {
41
+ "version": content["version"],
42
+ "slug": content["slug"],
43
+ "location": content["location"],
44
+ "featured": is_featured,
45
+ # frontmatter fields
46
+ "title": frontmatter.get("title", ""),
47
+ "description": frontmatter.get("description", ""),
48
+ "tags": frontmatter.get("tags", []),
49
+ "date": frontmatter.get("date", ""),
50
+ "thumbnail": frontmatter.get("thumbnail", ""),
51
+ "thumbnailOptimized": frontmatter.get("thumbnailOptimized", ""),
52
+ "authors": frontmatter.get("authors", []),
53
+ }
54
+
55
+
56
+ def convert_paths(content_type: tContentType, output_file: Path):
57
+ """
58
+ Convert paths (generated by MyST build) in the content_json file to use the new paths in the build output.
59
+ e.g. convert /blog_thumbnail-08d4f63a34eb7b55b6a00c25218c3295.png to /blog/build/blog_thumbnail-08d4f63a34eb7b55b6a00c25218c3295.png
60
+ """
61
+ with open(output_file, encoding="utf-8") as f:
62
+ content = json.load(f)
63
+
64
+ for item in content:
65
+ if not item.get("thumbnailOptimized"):
66
+ item["thumbnailOptimized"] = item["thumbnail"]
67
+ item["thumbnail"] = f"/{content_type}/build" + item["thumbnail"]
68
+ item["thumbnailOptimized"] = (
69
+ f"/{content_type}/build" + item["thumbnailOptimized"]
70
+ )
71
+
72
+ with open(output_file, "w", encoding="utf-8") as f:
73
+ json.dump(content, f, indent=2, ensure_ascii=False)
74
+
75
+ click.echo(f"Completed path conversion in {output_file}")
76
+
77
+
78
+ def build_content_json():
79
+ """Build {content_type}.json manifest for non-doc content types.
80
+
81
+ Reads all JSON files from afterpython/_build/{content_type}/site/content/
82
+ (excluding index.json) and combines them into a single manifest file at
83
+ afterpython/_build/{content_type}.json.
84
+
85
+ This manifest is used by SvelteKit landing pages to list all content items.
86
+
87
+ Args:
88
+ content_type: Content type (blog, tutorial, example, guide)
89
+ """
90
+ for content_type in CONTENT_TYPES:
91
+ if content_type == "doc":
92
+ continue # Doc doesn't need a combined manifest
93
+
94
+ content_path = ap.paths.afterpython_path / content_type
95
+ content_dir = content_path / "_build" / "site" / "content"
96
+ myst_yml_path = content_path / "myst.yml"
97
+ if (
98
+ not content_path.exists()
99
+ or not myst_yml_path.exists()
100
+ or not content_dir.exists()
101
+ ):
102
+ click.echo(f"Skipping {content_type}.json (missing files)")
103
+ continue
104
+
105
+ click.echo(f"Building {content_type}.json...")
106
+
107
+ # Collect all JSON files except index.json, which is created by a placeholder index.md file
108
+ content_items = []
109
+ for json_file in content_dir.glob("*.json"):
110
+ if json_file.stem == "index":
111
+ continue # Skip index.json
112
+
113
+ try:
114
+ with open(json_file, encoding="utf-8") as f:
115
+ data = json.load(f)
116
+
117
+ # Preserve all data from MyST JSON files
118
+ # The project-website-template will parse the fields it needs
119
+ content_items.append(standardize_data(content_type, data))
120
+ except (json.JSONDecodeError, KeyError) as e:
121
+ click.echo(f"Warning: Failed to parse {json_file}: {e}")
122
+ continue
123
+
124
+ # Write combined manifest
125
+ output_file = ap.paths.build_path / f"{content_type}.json"
126
+ with open(output_file, "w", encoding="utf-8") as f:
127
+ json.dump(content_items, f, indent=2, ensure_ascii=False)
128
+
129
+ click.echo(f"Generated {output_file} with {len(content_items)} items")
130
+
131
+ convert_paths(content_type, output_file)