afterpython 0.2.1__tar.gz → 0.3.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.
Files changed (64) hide show
  1. {afterpython-0.2.1 → afterpython-0.3.1}/PKG-INFO +21 -18
  2. {afterpython-0.2.1 → afterpython-0.3.1}/README.md +9 -7
  3. {afterpython-0.2.1 → afterpython-0.3.1}/pyproject.toml +19 -13
  4. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/builders/__init__.py +8 -0
  5. afterpython-0.3.1/src/afterpython/builders/faq_json.py +44 -0
  6. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/builders/jupyter_notebook.py +1 -14
  7. afterpython-0.3.1/src/afterpython/builders/llms_txt.py +248 -0
  8. afterpython-0.3.1/src/afterpython/builders/marimo_notebook.py +144 -0
  9. afterpython-0.3.1/src/afterpython/builders/metadata.py +83 -0
  10. afterpython-0.3.1/src/afterpython/builders/url_md.py +107 -0
  11. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/build.py +30 -0
  12. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/init.py +30 -11
  13. afterpython-0.3.1/src/afterpython/cli/commands/serve.py +29 -0
  14. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/sync.py +6 -0
  15. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/update.py +84 -4
  16. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/main.py +2 -0
  17. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/templates/ci-workflow-template.yml +4 -6
  18. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/templates/deploy-workflow-template.yml +1 -0
  19. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/tools/_afterpython.py +2 -0
  20. afterpython-0.3.1/src/afterpython/tools/github_actions.py +82 -0
  21. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/tools/pyproject.py +8 -2
  22. afterpython-0.2.1/src/afterpython/builders/metadata.py +0 -50
  23. afterpython-0.2.1/src/afterpython/tools/github_actions.py +0 -48
  24. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/__init__.py +0 -0
  25. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/_io/toml.py +0 -0
  26. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/_io/yaml.py +0 -0
  27. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/_paths.py +0 -0
  28. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/_typing.py +0 -0
  29. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/builders/content_json.py +0 -0
  30. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/builders/index_md.py +0 -0
  31. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/builders/markdown.py +0 -0
  32. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/__init__.py +0 -0
  33. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/add.py +0 -0
  34. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/bump.py +0 -0
  35. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/check.py +0 -0
  36. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/clean.py +0 -0
  37. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/commit.py +0 -0
  38. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/commitizen.py +0 -0
  39. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/dev.py +0 -0
  40. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/format.py +0 -0
  41. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/init_branch_rules.py +0 -0
  42. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/install.py +0 -0
  43. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/lock.py +0 -0
  44. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/pre_commit.py +0 -0
  45. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/preview.py +0 -0
  46. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/release.py +0 -0
  47. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/remove.py +0 -0
  48. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/cli/commands/start.py +0 -0
  49. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/const.py +0 -0
  50. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/main.py +0 -0
  51. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/pcu.py +0 -0
  52. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/py.typed +0 -0
  53. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/templates/cz-template.toml +0 -0
  54. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/templates/dependabot-template.yml +0 -0
  55. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/templates/pre-commit-config-template.yaml +0 -0
  56. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/templates/release-workflow-template.yml +0 -0
  57. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/templates/ruff-template.toml +0 -0
  58. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/tools/_git.py +0 -0
  59. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/tools/branch_rules.py +0 -0
  60. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/tools/commitizen.py +0 -0
  61. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/tools/myst.py +0 -0
  62. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/tools/pre_commit.py +0 -0
  63. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/utils.py +0 -0
  64. {afterpython-0.2.1 → afterpython-0.3.1}/src/afterpython/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: afterpython
3
- Version: 0.2.1
3
+ Version: 0.3.1
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.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.8.0
26
- Requires-Dist: pdoc>=16.0.0
27
- Requires-Dist: ruff>=0.14.14
28
- Requires-Dist: pre-commit>=4.5.1
29
- Requires-Dist: commitizen>=4.12.1
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
  [![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)
40
+ [![Downloads](https://static.pepy.tech/badge/afterpython/month)](https://pepy.tech/project/afterpython)
41
+ [![PyPI](https://img.shields.io/pypi/v/afterpython.svg?cacheSeconds=300)](https://pypi.org/project/afterpython)
42
+ ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython?cacheSeconds=300)
42
43
  [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
43
44
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/AfterPythonOrg/afterpython)
45
+ [![View Code Wiki](https://assets.codewiki.google/readme-badge/static.svg)](https://codewiki.google/github.com/afterpythonorg/afterpython?utm_source=badge&utm_medium=github&utm_campaign=github.com/afterpythonorg/afterpython)
46
+ <!-- ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython) -->
44
47
  <!-- [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](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
- [pdoc]: https://pdoc.dev/
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 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.
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
- - [pdoc]
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
  [![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)
4
+ [![Downloads](https://static.pepy.tech/badge/afterpython/month)](https://pepy.tech/project/afterpython)
5
+ [![PyPI](https://img.shields.io/pypi/v/afterpython.svg?cacheSeconds=300)](https://pypi.org/project/afterpython)
6
+ ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython?cacheSeconds=300)
7
7
  [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
8
8
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/AfterPythonOrg/afterpython)
9
+ [![View Code Wiki](https://assets.codewiki.google/readme-badge/static.svg)](https://codewiki.google/github.com/afterpythonorg/afterpython?utm_source=badge&utm_medium=github&utm_campaign=github.com/afterpythonorg/afterpython)
10
+ <!-- ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython) -->
9
11
  <!-- [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](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
- [pdoc]: https://pdoc.dev/
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 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.
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
- - [pdoc]
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.2.1"
3
+ version = "0.3.1"
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.13.3",
39
- "ruamel-yaml>=0.18.16",
40
- "gitpython>=3.1.45",
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.1",
43
- "pyproject-metadata>=0.10.0",
44
- # "jupytext>=1.18.1",
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.8.0",
47
- "pdoc>=16.0.0",
48
- "ruff>=0.14.14",
49
- "pre-commit>=4.5.1",
50
- "commitizen>=4.12.1",
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.9.26,<0.10.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,44 @@
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
+ raw = read_yaml(faq_yml_path)
28
+ if raw is None:
29
+ click.echo("faq.yml is empty, skip building faq.json")
30
+ return
31
+
32
+ click.echo("Building faq.json...")
33
+
34
+ items = [
35
+ {
36
+ "question": str(item["question"]),
37
+ "answer": str(item["answer"]),
38
+ "category": str(item["category"]) if item.get("category") else "",
39
+ }
40
+ for item in raw
41
+ ]
42
+
43
+ with open(build_path / "faq.json", "w") as f:
44
+ 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: ![alt](url)
40
+ # [![alt](url)](link)
41
+ # <!-- ![alt](url) -->
42
+ # <!-- [![alt](url)](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)")