afterpython 0.1.1__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.
- afterpython-0.1.1/PKG-INFO +131 -0
- afterpython-0.1.1/README.md +96 -0
- afterpython-0.1.1/pyproject.toml +73 -0
- afterpython-0.1.1/src/afterpython/__init__.py +13 -0
- afterpython-0.1.1/src/afterpython/_io/toml.py +105 -0
- afterpython-0.1.1/src/afterpython/_io/yaml.py +26 -0
- afterpython-0.1.1/src/afterpython/_paths.py +39 -0
- afterpython-0.1.1/src/afterpython/_typing.py +4 -0
- afterpython-0.1.1/src/afterpython/builders/__init__.py +17 -0
- afterpython-0.1.1/src/afterpython/builders/content_json.py +131 -0
- afterpython-0.1.1/src/afterpython/builders/index_md.py +94 -0
- afterpython-0.1.1/src/afterpython/builders/jupyter_notebook.py +293 -0
- afterpython-0.1.1/src/afterpython/builders/markdown.py +271 -0
- afterpython-0.1.1/src/afterpython/builders/metadata.py +50 -0
- afterpython-0.1.1/src/afterpython/cli/__init__.py +3 -0
- afterpython-0.1.1/src/afterpython/cli/commands/add.py +67 -0
- afterpython-0.1.1/src/afterpython/cli/commands/build.py +260 -0
- afterpython-0.1.1/src/afterpython/cli/commands/bump.py +118 -0
- afterpython-0.1.1/src/afterpython/cli/commands/check.py +36 -0
- afterpython-0.1.1/src/afterpython/cli/commands/clean.py +85 -0
- afterpython-0.1.1/src/afterpython/cli/commands/commit.py +52 -0
- afterpython-0.1.1/src/afterpython/cli/commands/commitizen.py +32 -0
- afterpython-0.1.1/src/afterpython/cli/commands/dev.py +213 -0
- afterpython-0.1.1/src/afterpython/cli/commands/format.py +28 -0
- afterpython-0.1.1/src/afterpython/cli/commands/init.py +107 -0
- afterpython-0.1.1/src/afterpython/cli/commands/init_branch_rules.py +9 -0
- afterpython-0.1.1/src/afterpython/cli/commands/install.py +28 -0
- afterpython-0.1.1/src/afterpython/cli/commands/lock.py +23 -0
- afterpython-0.1.1/src/afterpython/cli/commands/pre_commit.py +66 -0
- afterpython-0.1.1/src/afterpython/cli/commands/preview.py +22 -0
- afterpython-0.1.1/src/afterpython/cli/commands/release.py +67 -0
- afterpython-0.1.1/src/afterpython/cli/commands/remove.py +67 -0
- afterpython-0.1.1/src/afterpython/cli/commands/start.py +144 -0
- afterpython-0.1.1/src/afterpython/cli/commands/sync.py +152 -0
- afterpython-0.1.1/src/afterpython/cli/commands/update.py +176 -0
- afterpython-0.1.1/src/afterpython/cli/main.py +132 -0
- afterpython-0.1.1/src/afterpython/const.py +4 -0
- afterpython-0.1.1/src/afterpython/main.py +25 -0
- afterpython-0.1.1/src/afterpython/pcu.py +166 -0
- afterpython-0.1.1/src/afterpython/templates/ci-workflow-template.yml +197 -0
- afterpython-0.1.1/src/afterpython/templates/cz-template.toml +7 -0
- afterpython-0.1.1/src/afterpython/templates/dependabot-template.yml +30 -0
- afterpython-0.1.1/src/afterpython/templates/deploy-workflow-template.yml +88 -0
- afterpython-0.1.1/src/afterpython/templates/pre-commit-config-template.yaml +48 -0
- afterpython-0.1.1/src/afterpython/templates/release-workflow-template.yml +56 -0
- afterpython-0.1.1/src/afterpython/templates/ruff-template.toml +32 -0
- afterpython-0.1.1/src/afterpython/tools/_afterpython.py +128 -0
- afterpython-0.1.1/src/afterpython/tools/_git.py +137 -0
- afterpython-0.1.1/src/afterpython/tools/branch_rules.py +106 -0
- afterpython-0.1.1/src/afterpython/tools/commitizen.py +13 -0
- afterpython-0.1.1/src/afterpython/tools/github_actions.py +48 -0
- afterpython-0.1.1/src/afterpython/tools/myst.py +208 -0
- afterpython-0.1.1/src/afterpython/tools/pre_commit.py +37 -0
- afterpython-0.1.1/src/afterpython/tools/pyproject.py +110 -0
- afterpython-0.1.1/src/afterpython/utils.py +299 -0
- afterpython-0.1.1/src/afterpython/version.py +39 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: afterpython
|
|
3
|
+
Version: 0.1.1
|
|
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
|
+
[](https://afterpython.org)
|
|
39
|
+

|
|
40
|
+
[](https://pypi.org/project/afterpython)
|
|
41
|
+

|
|
42
|
+
[](https://github.com/AfterPythonOrg/afterpython/discussions)
|
|
43
|
+
[](https://deepwiki.com/AfterPythonOrg/afterpython)
|
|
44
|
+
<!-- [](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
|
+
[](https://afterpython.org)
|
|
4
|
+

|
|
5
|
+
[](https://pypi.org/project/afterpython)
|
|
6
|
+

|
|
7
|
+
[](https://github.com/AfterPythonOrg/afterpython/discussions)
|
|
8
|
+
[](https://deepwiki.com/AfterPythonOrg/afterpython)
|
|
9
|
+
<!-- [](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.1"
|
|
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,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)
|