afterpython 0.2.0__tar.gz → 0.3.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.
- {afterpython-0.2.0 → afterpython-0.3.0}/PKG-INFO +21 -18
- {afterpython-0.2.0 → afterpython-0.3.0}/README.md +9 -7
- {afterpython-0.2.0 → afterpython-0.3.0}/pyproject.toml +19 -13
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/builders/__init__.py +8 -0
- afterpython-0.3.0/src/afterpython/builders/faq_json.py +43 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/builders/jupyter_notebook.py +1 -14
- afterpython-0.3.0/src/afterpython/builders/llms_txt.py +248 -0
- afterpython-0.3.0/src/afterpython/builders/marimo_notebook.py +144 -0
- afterpython-0.3.0/src/afterpython/builders/metadata.py +83 -0
- afterpython-0.3.0/src/afterpython/builders/url_md.py +107 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/build.py +30 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/init.py +30 -11
- afterpython-0.3.0/src/afterpython/cli/commands/serve.py +29 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/sync.py +6 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/update.py +84 -4
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/main.py +2 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/templates/ci-workflow-template.yml +16 -9
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/templates/deploy-workflow-template.yml +1 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/tools/_afterpython.py +2 -0
- afterpython-0.3.0/src/afterpython/tools/github_actions.py +82 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/tools/pyproject.py +8 -2
- afterpython-0.2.0/src/afterpython/builders/metadata.py +0 -50
- afterpython-0.2.0/src/afterpython/tools/github_actions.py +0 -48
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/__init__.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/_io/toml.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/_io/yaml.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/_paths.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/_typing.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/builders/content_json.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/builders/index_md.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/builders/markdown.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/__init__.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/add.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/bump.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/check.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/clean.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/commit.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/commitizen.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/dev.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/format.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/init_branch_rules.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/install.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/lock.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/pre_commit.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/preview.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/release.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/remove.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/cli/commands/start.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/const.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/main.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/pcu.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/py.typed +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/templates/cz-template.toml +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/templates/dependabot-template.yml +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/templates/pre-commit-config-template.yaml +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/templates/release-workflow-template.yml +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/templates/ruff-template.toml +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/tools/_git.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/tools/branch_rules.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/tools/commitizen.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/tools/myst.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/tools/pre_commit.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/utils.py +0 -0
- {afterpython-0.2.0 → afterpython-0.3.0}/src/afterpython/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: afterpython
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: CLI tool to streamline Python package maintenance and generate a dedicated project website
|
|
5
5
|
Keywords: package-maintenance,project-website,static-site-generator,developer-tools,documentation,cli-tool,ci/cd,mystmd,semantic-release,python-packaging
|
|
6
6
|
Author: Stephen Yau
|
|
@@ -17,16 +17,17 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
17
17
|
Requires-Dist: click>=8.2.1
|
|
18
18
|
Requires-Dist: trogon>=0.6.0
|
|
19
19
|
Requires-Dist: httpx>=0.28.1
|
|
20
|
-
Requires-Dist: tomlkit>=0.
|
|
21
|
-
Requires-Dist: ruamel-yaml>=0.
|
|
22
|
-
Requires-Dist: gitpython>=3.1.
|
|
23
|
-
Requires-Dist: python-dotenv>=1.2.
|
|
24
|
-
Requires-Dist: pyproject-metadata>=0.
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
20
|
+
Requires-Dist: tomlkit>=0.14.0
|
|
21
|
+
Requires-Dist: ruamel-yaml>=0.19.1
|
|
22
|
+
Requires-Dist: gitpython>=3.1.49
|
|
23
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
24
|
+
Requires-Dist: pyproject-metadata>=0.11.0
|
|
25
|
+
Requires-Dist: jupytext>=1.19.1
|
|
26
|
+
Requires-Dist: mystmd>=1.9.0
|
|
27
|
+
Requires-Dist: great-docs>=0.9
|
|
28
|
+
Requires-Dist: ruff>=0.15.12
|
|
29
|
+
Requires-Dist: pre-commit>=4.6.0
|
|
30
|
+
Requires-Dist: commitizen>=4.13.10
|
|
30
31
|
Requires-Python: >=3.11
|
|
31
32
|
Project-URL: homepage, https://afterpython.afterpython.org
|
|
32
33
|
Project-URL: repository, https://github.com/AfterPythonOrg/afterpython
|
|
@@ -36,11 +37,13 @@ Description-Content-Type: text/markdown
|
|
|
36
37
|
# AfterPython: Python Package Maintenance Toolkit and Project Website Generator
|
|
37
38
|
|
|
38
39
|
[](https://afterpython.org)
|
|
39
|
-
](https://pypi.org/project/afterpython)
|
|
41
|
-

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

|
|
42
43
|
[](https://github.com/AfterPythonOrg/afterpython/discussions)
|
|
43
44
|
[](https://deepwiki.com/AfterPythonOrg/afterpython)
|
|
45
|
+
[](https://codewiki.google/github.com/afterpythonorg/afterpython?utm_source=badge&utm_medium=github&utm_campaign=github.com/afterpythonorg/afterpython)
|
|
46
|
+
<!--  -->
|
|
44
47
|
<!-- [](https://pixi.sh) -->
|
|
45
48
|
|
|
46
49
|
|
|
@@ -50,7 +53,7 @@ Description-Content-Type: text/markdown
|
|
|
50
53
|
[pre-commit]: https://pre-commit.com
|
|
51
54
|
[pagefind]: https://pagefind.app
|
|
52
55
|
[SemVer]: https://semver.org
|
|
53
|
-
[
|
|
56
|
+
[great-docs]: https://posit-dev.github.io/great-docs/
|
|
54
57
|
[GitHub Actions]: https://github.com/features/actions
|
|
55
58
|
[PyTorch]: https://pytorch.org
|
|
56
59
|
[Scikit-Learn]: https://scikit-learn.org
|
|
@@ -63,7 +66,7 @@ Description-Content-Type: text/markdown
|
|
|
63
66
|
|
|
64
67
|
## Problem
|
|
65
68
|
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
|
|
69
|
+
First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [SemVer], CI/CD ([pre-commit] hooks, GitHub workflows), and they often spend 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
70
|
|
|
68
71
|
|
|
69
72
|
## Solution
|
|
@@ -78,8 +81,8 @@ First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [Sem
|
|
|
78
81
|
- [x] Go from writing to **website deployment in minutes** — no need to learn any of the underlying tools
|
|
79
82
|
- [x] Centralize all your content in a modern, **unified project website** — from documentation to blog posts
|
|
80
83
|
- [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**
|
|
84
|
+
- [x] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
|
|
81
85
|
- [ ] 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
86
|
- [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
|
|
84
87
|
|
|
85
88
|
---
|
|
@@ -121,7 +124,7 @@ ap tui
|
|
|
121
124
|
- [project-website-template]
|
|
122
125
|
- [pre-commit]
|
|
123
126
|
- [GitHub Actions]
|
|
124
|
-
- [
|
|
127
|
+
- [great-docs]
|
|
125
128
|
- [commitizen]
|
|
126
129
|
- [uv]
|
|
127
130
|
- [ruff]
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# AfterPython: Python Package Maintenance Toolkit and Project Website Generator
|
|
2
2
|
|
|
3
3
|
[](https://afterpython.org)
|
|
4
|
-
](https://pypi.org/project/afterpython)
|
|
6
|
-

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

|
|
7
7
|
[](https://github.com/AfterPythonOrg/afterpython/discussions)
|
|
8
8
|
[](https://deepwiki.com/AfterPythonOrg/afterpython)
|
|
9
|
+
[](https://codewiki.google/github.com/afterpythonorg/afterpython?utm_source=badge&utm_medium=github&utm_campaign=github.com/afterpythonorg/afterpython)
|
|
10
|
+
<!--  -->
|
|
9
11
|
<!-- [](https://pixi.sh) -->
|
|
10
12
|
|
|
11
13
|
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
[pre-commit]: https://pre-commit.com
|
|
16
18
|
[pagefind]: https://pagefind.app
|
|
17
19
|
[SemVer]: https://semver.org
|
|
18
|
-
[
|
|
20
|
+
[great-docs]: https://posit-dev.github.io/great-docs/
|
|
19
21
|
[GitHub Actions]: https://github.com/features/actions
|
|
20
22
|
[PyTorch]: https://pytorch.org
|
|
21
23
|
[Scikit-Learn]: https://scikit-learn.org
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
|
|
29
31
|
## Problem
|
|
30
32
|
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
|
|
33
|
+
First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [SemVer], CI/CD ([pre-commit] hooks, GitHub workflows), and they often spend 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
34
|
|
|
33
35
|
|
|
34
36
|
## Solution
|
|
@@ -43,8 +45,8 @@ First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [Sem
|
|
|
43
45
|
- [x] Go from writing to **website deployment in minutes** — no need to learn any of the underlying tools
|
|
44
46
|
- [x] Centralize all your content in a modern, **unified project website** — from documentation to blog posts
|
|
45
47
|
- [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**
|
|
48
|
+
- [x] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
|
|
46
49
|
- [ ] 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
50
|
- [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
|
|
49
51
|
|
|
50
52
|
---
|
|
@@ -86,7 +88,7 @@ ap tui
|
|
|
86
88
|
- [project-website-template]
|
|
87
89
|
- [pre-commit]
|
|
88
90
|
- [GitHub Actions]
|
|
89
|
-
- [
|
|
91
|
+
- [great-docs]
|
|
90
92
|
- [commitizen]
|
|
91
93
|
- [uv]
|
|
92
94
|
- [ruff]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "afterpython"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "CLI tool to streamline Python package maintenance and generate a dedicated project website"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
authors = [
|
|
@@ -35,19 +35,19 @@ dependencies = [
|
|
|
35
35
|
"click>=8.2.1",
|
|
36
36
|
"trogon>=0.6.0",
|
|
37
37
|
"httpx>=0.28.1",
|
|
38
|
-
"tomlkit>=0.
|
|
39
|
-
"ruamel-yaml>=0.
|
|
40
|
-
"gitpython>=3.1.
|
|
38
|
+
"tomlkit>=0.14.0",
|
|
39
|
+
"ruamel-yaml>=0.19.1",
|
|
40
|
+
"gitpython>=3.1.49",
|
|
41
41
|
# "pygithub>=2.8.1",
|
|
42
|
-
"python-dotenv>=1.2.
|
|
43
|
-
"pyproject-metadata>=0.
|
|
44
|
-
|
|
42
|
+
"python-dotenv>=1.2.2",
|
|
43
|
+
"pyproject-metadata>=0.11.0",
|
|
44
|
+
"jupytext>=1.19.1",
|
|
45
45
|
# "jinja2>=3.1.6",
|
|
46
|
-
"mystmd>=1.
|
|
47
|
-
"
|
|
48
|
-
"ruff>=0.
|
|
49
|
-
"pre-commit>=4.
|
|
50
|
-
"commitizen>=4.
|
|
46
|
+
"mystmd>=1.9.0",
|
|
47
|
+
"great-docs>=0.9",
|
|
48
|
+
"ruff>=0.15.12",
|
|
49
|
+
"pre-commit>=4.6.0",
|
|
50
|
+
"commitizen>=4.13.10",
|
|
51
51
|
# "python-semantic-release>=10.5.2",
|
|
52
52
|
]
|
|
53
53
|
|
|
@@ -64,9 +64,15 @@ pcu = "afterpython.main:run_pcu"
|
|
|
64
64
|
# override-dependencies = ["platformdirs>=4.5.0", "nodeenv>=1.9.1"]
|
|
65
65
|
|
|
66
66
|
[build-system]
|
|
67
|
-
requires = ["uv_build>=0.
|
|
67
|
+
requires = ["uv_build>=0.11.8,<0.12.0"]
|
|
68
68
|
build-backend = "uv_build"
|
|
69
69
|
|
|
70
70
|
[tool.uv.build-backend]
|
|
71
71
|
module-name = "afterpython"
|
|
72
72
|
module-root = "src"
|
|
73
|
+
|
|
74
|
+
[tool.uv.sources]
|
|
75
|
+
afterpython = { path = ".", editable = true }
|
|
76
|
+
|
|
77
|
+
[tool.pyright]
|
|
78
|
+
typeCheckingMode = "basic"
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
from afterpython.builders.content_json import build_content_json
|
|
2
|
+
from afterpython.builders.faq_json import build_faq_json
|
|
2
3
|
from afterpython.builders.index_md import (
|
|
3
4
|
create_placeholder_index_md_files,
|
|
4
5
|
delete_placeholder_index_md_files,
|
|
5
6
|
)
|
|
6
7
|
from afterpython.builders.jupyter_notebook import build_jupyter_notebooks
|
|
8
|
+
from afterpython.builders.llms_txt import build_llms_txt
|
|
9
|
+
from afterpython.builders.marimo_notebook import build_marimo_readme
|
|
7
10
|
from afterpython.builders.markdown import build_markdown
|
|
8
11
|
from afterpython.builders.metadata import build_metadata
|
|
12
|
+
from afterpython.builders.url_md import build_url_md
|
|
9
13
|
|
|
10
14
|
__all__ = (
|
|
11
15
|
"build_content_json",
|
|
16
|
+
"build_faq_json",
|
|
12
17
|
"build_jupyter_notebooks",
|
|
18
|
+
"build_llms_txt",
|
|
19
|
+
"build_marimo_readme",
|
|
13
20
|
"build_markdown",
|
|
14
21
|
"build_metadata",
|
|
22
|
+
"build_url_md",
|
|
15
23
|
"create_placeholder_index_md_files",
|
|
16
24
|
"delete_placeholder_index_md_files",
|
|
17
25
|
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
import afterpython as ap
|
|
8
|
+
from afterpython._io.yaml import read_yaml
|
|
9
|
+
|
|
10
|
+
build_path = ap.paths.build_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_faq_json():
|
|
14
|
+
"""Build faq.json from the user's afterpython/faq.yml.
|
|
15
|
+
|
|
16
|
+
Output shape: a flat list of {question, answer, category}. `category` is
|
|
17
|
+
always present — empty string when unset — so the frontend can rely on a
|
|
18
|
+
uniform item shape. Order is preserved so the frontend can derive category
|
|
19
|
+
order from first appearance.
|
|
20
|
+
"""
|
|
21
|
+
faq_yml_path = ap.paths.afterpython_path / "faq.yml"
|
|
22
|
+
|
|
23
|
+
if not faq_yml_path.exists():
|
|
24
|
+
click.echo("No faq.yml found, skip building faq.json")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
click.echo("Building faq.json...")
|
|
28
|
+
|
|
29
|
+
raw = read_yaml(faq_yml_path)
|
|
30
|
+
if raw is None:
|
|
31
|
+
items: list[dict] = []
|
|
32
|
+
else:
|
|
33
|
+
items = [
|
|
34
|
+
{
|
|
35
|
+
"question": str(item["question"]),
|
|
36
|
+
"answer": str(item["answer"]),
|
|
37
|
+
"category": str(item["category"]) if item.get("category") else "",
|
|
38
|
+
}
|
|
39
|
+
for item in raw
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
with open(build_path / "faq.json", "w") as f:
|
|
43
|
+
json.dump(items, f, indent=2)
|
|
@@ -5,24 +5,11 @@ from pathlib import Path
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
7
|
import afterpython as ap
|
|
8
|
+
from afterpython.builders.marimo_notebook import _create_molab_url, _get_molab_badge
|
|
8
9
|
from afterpython.const import CONTENT_TYPES
|
|
9
10
|
from afterpython.tools.pyproject import read_metadata
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def _get_molab_badge() -> str:
|
|
13
|
-
return "https://marimo.io/molab-shield.svg"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _create_molab_url(github_url: str, content_path: Path):
|
|
17
|
-
"""Create a molab URL for a given content type and notebook path.
|
|
18
|
-
Args:
|
|
19
|
-
github_url: str, e.g. "https://github.com/AfterPythonOrg/afterpython"
|
|
20
|
-
content_path: str, e.g. "tutorial/test.ipynb"
|
|
21
|
-
"""
|
|
22
|
-
github_url = github_url.replace("https://github.com/", "github/")
|
|
23
|
-
return f"https://molab.marimo.io/{github_url}/blob/main/afterpython/{content_path.as_posix()}"
|
|
24
|
-
|
|
25
|
-
|
|
26
13
|
def _read_notebook(notebook_path: Path, content_path: Path) -> dict | None:
|
|
27
14
|
"""Read and validate a Jupyter notebook.
|
|
28
15
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# VIBE-CODED
|
|
2
|
+
"""Build llms.txt — a single catalog of the site for AI agents.
|
|
3
|
+
|
|
4
|
+
Output: `afterpython/_website/static/llms.txt`. SvelteKit's static adapter
|
|
5
|
+
serves it at `/llms.txt` so agents (Claude, GPT, etc.) can fetch a curated
|
|
6
|
+
overview of the project + a link list of every published page.
|
|
7
|
+
|
|
8
|
+
Conforms to the llms.txt convention (https://llmstxt.org/): a markdown file
|
|
9
|
+
with a project H1, optional context paragraphs, and `## Section` headings
|
|
10
|
+
each containing a link list.
|
|
11
|
+
|
|
12
|
+
Flow
|
|
13
|
+
----
|
|
14
|
+
build_llms_txt() # entry point
|
|
15
|
+
├─ _read_readme() + _standardize_readme() # → header content
|
|
16
|
+
└─ for each content_type in ORDERED_CONTENT_TYPES:
|
|
17
|
+
└─ _list_pages(content_type) # → [(title, url, abstract)]
|
|
18
|
+
(skips entries whose source .md no longer exists — those were
|
|
19
|
+
auto-generated placeholder index.md files deleted in postbuild)
|
|
20
|
+
└─ _format_section(content_type, blurb, entries)
|
|
21
|
+
├─ heading: SECTION_HEADINGS[content_type]
|
|
22
|
+
├─ blurb: project.description from afterpython/{type}/myst.yml
|
|
23
|
+
└─ entries: `- [Title](/type/slug.md): abstract`
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
|
|
33
|
+
import afterpython as ap
|
|
34
|
+
from afterpython._typing import tContentType
|
|
35
|
+
from afterpython.const import CONTENT_TYPES
|
|
36
|
+
|
|
37
|
+
# A line whose entire content is a markdown image (badge), with optional
|
|
38
|
+
# link wrapper and optional HTML-comment wrapper.
|
|
39
|
+
# matches: 
|
|
40
|
+
# [](link)
|
|
41
|
+
# <!--  -->
|
|
42
|
+
# <!-- [](link) -->
|
|
43
|
+
# Anchored ^...$ so inline references in body prose are NOT matched.
|
|
44
|
+
_BADGE_LINE = re.compile(
|
|
45
|
+
r"""
|
|
46
|
+
^\s* # leading whitespace
|
|
47
|
+
(?:<!--\s*)? # optional HTML comment open
|
|
48
|
+
\[? # optional link open '['
|
|
49
|
+
!\[[^\]]*\] # ![alt]
|
|
50
|
+
\([^)]+\) # (image_url)
|
|
51
|
+
(?:\]\([^)]+\))? # optional ](link_url)
|
|
52
|
+
(?:\s*-->)? # optional HTML comment close
|
|
53
|
+
\s*$ # trailing whitespace
|
|
54
|
+
""",
|
|
55
|
+
re.VERBOSE,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Order in which content-type sections appear in llms.txt. Sections with zero
|
|
59
|
+
# entries are skipped. Reasoning: an agent answering "how do I X?" should hit
|
|
60
|
+
# concepts first (doc), then how-to (tutorial/guide), then examples, then
|
|
61
|
+
# background (blog) — strongest signal first.
|
|
62
|
+
ORDERED_CONTENT_TYPES: list[tContentType] = [
|
|
63
|
+
"doc",
|
|
64
|
+
"tutorial",
|
|
65
|
+
"guide",
|
|
66
|
+
"example",
|
|
67
|
+
"blog",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# One-line hint that sits between the README and the section list, telling
|
|
71
|
+
# an LLM reading llms.txt that the URLs below are fetchable markdown sources
|
|
72
|
+
# (the Mintlify / Anthropic-docs convention). Costs ~20 tokens and removes
|
|
73
|
+
# any ambiguity about what fetching `/doc/quickstart.md` returns.
|
|
74
|
+
LINK_NOTE = (
|
|
75
|
+
"> Every link below points to the raw markdown source of the page."
|
|
76
|
+
" Fetch any URL directly to read its content as markdown."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Human-readable display name for each content type's `## ` heading in llms.txt.
|
|
80
|
+
SECTION_HEADINGS: dict[tContentType, str] = {
|
|
81
|
+
"doc": "Documentation",
|
|
82
|
+
"tutorial": "Tutorials",
|
|
83
|
+
"guide": "Guides",
|
|
84
|
+
"example": "Examples",
|
|
85
|
+
"blog": "Blog",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Defensive sync check: if a new content type is added to const.CONTENT_TYPES,
|
|
89
|
+
# the developer must remember to slot it into ORDERED_CONTENT_TYPES and
|
|
90
|
+
# SECTION_HEADINGS. Import-time failure beats a silently-missing section.
|
|
91
|
+
assert set(ORDERED_CONTENT_TYPES) == CONTENT_TYPES, (
|
|
92
|
+
"ORDERED_CONTENT_TYPES is out of sync with const.CONTENT_TYPES"
|
|
93
|
+
)
|
|
94
|
+
assert set(SECTION_HEADINGS.keys()) == CONTENT_TYPES, (
|
|
95
|
+
"SECTION_HEADINGS is out of sync with const.CONTENT_TYPES"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _read_readme() -> str:
|
|
100
|
+
"""Read the project's README.md from repo root, verbatim. Empty string if
|
|
101
|
+
the README is missing (e.g. brand new project)."""
|
|
102
|
+
readme_path = ap.paths.user_path / "README.md"
|
|
103
|
+
if not readme_path.exists():
|
|
104
|
+
return ""
|
|
105
|
+
return readme_path.read_text(encoding="utf-8")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _standardize_readme(text: str) -> str:
|
|
109
|
+
"""Clean the README before embedding it in llms.txt.
|
|
110
|
+
|
|
111
|
+
Currently strips badges (shields, download counters, etc.) from the
|
|
112
|
+
README's *header zone* — everything before the first `## ` heading —
|
|
113
|
+
since they're chrome for human readers and noise for LLMs.
|
|
114
|
+
|
|
115
|
+
The header-zone restriction protects content images that legitimately
|
|
116
|
+
appear later in the README (e.g. the `<picture>` website screenshot
|
|
117
|
+
after the first H2 — also untouched because it's HTML, not a markdown
|
|
118
|
+
image).
|
|
119
|
+
|
|
120
|
+
Future ideas if more cleanup is wanted: strip GitHub-flavored anchor
|
|
121
|
+
links, install commands, "table of contents" sections.
|
|
122
|
+
"""
|
|
123
|
+
lines = text.split("\n")
|
|
124
|
+
|
|
125
|
+
# Boundary of the header zone: index of the first `## ` line, or end of
|
|
126
|
+
# file if no H2 exists.
|
|
127
|
+
boundary = next(
|
|
128
|
+
(i for i, line in enumerate(lines) if line.startswith("## ")),
|
|
129
|
+
len(lines),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
cleaned_header = [line for line in lines[:boundary] if not _BADGE_LINE.match(line)]
|
|
133
|
+
rejoined = "\n".join(cleaned_header + lines[boundary:])
|
|
134
|
+
|
|
135
|
+
# After removing 9 consecutive badge lines, you can end up with a stack
|
|
136
|
+
# of blanks where they used to be. Collapse 3+ newlines to 2 (one blank).
|
|
137
|
+
return re.sub(r"\n{3,}", "\n\n", rejoined)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _read_section_blurb(content_type: tContentType) -> str | None:
|
|
141
|
+
"""Pull the section's one-line blurb from `afterpython/{type}/myst.yml`'s
|
|
142
|
+
`project.description`.
|
|
143
|
+
|
|
144
|
+
Returns None if myst.yml is missing or has no description set — in which
|
|
145
|
+
case the section in llms.txt simply omits the `> blurb` line.
|
|
146
|
+
"""
|
|
147
|
+
from afterpython._io.yaml import read_yaml
|
|
148
|
+
|
|
149
|
+
myst_yml = ap.paths.afterpython_path / content_type / "myst.yml"
|
|
150
|
+
if not myst_yml.exists():
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
data = read_yaml(myst_yml) or {}
|
|
154
|
+
description = data.get("project", {}).get("description")
|
|
155
|
+
return str(description).strip() if description else None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _list_pages(content_type: tContentType) -> list[tuple[str, str, str | None]]:
|
|
159
|
+
"""Return `[(title, url, abstract), ...]` for each page MyST built.
|
|
160
|
+
|
|
161
|
+
Reads MyST's `_build/site/content/*.json` files (which already have
|
|
162
|
+
title, slug, and frontmatter resolved — no need to re-parse source).
|
|
163
|
+
Skips entries whose source .md no longer exists; those were auto-generated
|
|
164
|
+
placeholder index.md files that `delete_placeholder_index_md_files`
|
|
165
|
+
removed at the start of postbuild.
|
|
166
|
+
"""
|
|
167
|
+
content_dir = ap.paths.afterpython_path / content_type
|
|
168
|
+
myst_content_dir = content_dir / "_build" / "site" / "content"
|
|
169
|
+
if not myst_content_dir.exists():
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
pages: list[tuple[str, str, str | None]] = []
|
|
173
|
+
for json_file in sorted(myst_content_dir.glob("*.json")):
|
|
174
|
+
try:
|
|
175
|
+
data = json.loads(json_file.read_text(encoding="utf-8"))
|
|
176
|
+
except json.JSONDecodeError:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Filter placeholder index.md entries: their source has been deleted.
|
|
180
|
+
location = data.get("location", "")
|
|
181
|
+
source_md = content_dir / location.lstrip("/")
|
|
182
|
+
if not source_md.exists():
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
slug = data.get("slug")
|
|
186
|
+
if not slug:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
frontmatter = data.get("frontmatter", {})
|
|
190
|
+
title = frontmatter.get("title") or slug
|
|
191
|
+
abstract = frontmatter.get("abstract")
|
|
192
|
+
|
|
193
|
+
url = f"/{content_type}/{slug}.md"
|
|
194
|
+
pages.append((title, url, abstract))
|
|
195
|
+
return pages
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _format_link_entry(title: str, url: str, abstract: str | None) -> str:
|
|
199
|
+
"""One line of the link list.
|
|
200
|
+
|
|
201
|
+
With abstract: `- [Title](url): abstract`
|
|
202
|
+
Without abstract: `- [Title](url)`
|
|
203
|
+
"""
|
|
204
|
+
if abstract:
|
|
205
|
+
return f"- [{title}]({url}): {abstract.strip()}"
|
|
206
|
+
return f"- [{title}]({url})"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _format_section(
|
|
210
|
+
content_type: tContentType,
|
|
211
|
+
blurb: str | None,
|
|
212
|
+
entries: list[tuple[str, str, str | None]],
|
|
213
|
+
) -> str:
|
|
214
|
+
"""Format one `## Section`: heading, optional blurb, link list."""
|
|
215
|
+
parts = [f"## {SECTION_HEADINGS[content_type]}", ""]
|
|
216
|
+
if blurb:
|
|
217
|
+
parts.extend([f"> {blurb}", ""])
|
|
218
|
+
for title, url, abstract in entries:
|
|
219
|
+
parts.append(_format_link_entry(title, url, abstract))
|
|
220
|
+
return "\n".join(parts)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def build_llms_txt() -> None:
|
|
224
|
+
"""Generate `_website/static/llms.txt` — one catalog file for the site.
|
|
225
|
+
|
|
226
|
+
Runs at the end of `postbuild()` after MyST output is finalized and
|
|
227
|
+
placeholder index.md files have been removed.
|
|
228
|
+
"""
|
|
229
|
+
readme = _standardize_readme(_read_readme())
|
|
230
|
+
|
|
231
|
+
sections: list[str] = []
|
|
232
|
+
total_entries = 0
|
|
233
|
+
for content_type in ORDERED_CONTENT_TYPES:
|
|
234
|
+
entries = _list_pages(content_type)
|
|
235
|
+
if not entries:
|
|
236
|
+
continue
|
|
237
|
+
blurb = _read_section_blurb(content_type)
|
|
238
|
+
sections.append(_format_section(content_type, blurb, entries))
|
|
239
|
+
total_entries += len(entries)
|
|
240
|
+
|
|
241
|
+
body = readme.rstrip()
|
|
242
|
+
if sections:
|
|
243
|
+
body = body + "\n\n" + LINK_NOTE + "\n\n" + "\n\n".join(sections) + "\n"
|
|
244
|
+
|
|
245
|
+
output = ap.paths.website_path / "static" / "llms.txt"
|
|
246
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
output.write_text(body, encoding="utf-8")
|
|
248
|
+
click.echo(f"Generated {output} ({total_entries} entries)")
|