afterpython 0.1.0__tar.gz → 0.1.0.dev5__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 (66) hide show
  1. {afterpython-0.1.0 → afterpython-0.1.0.dev5}/PKG-INFO +13 -39
  2. {afterpython-0.1.0 → afterpython-0.1.0.dev5}/README.md +3 -30
  3. afterpython-0.1.0/src/afterpython/templates/pre-commit-config-template.yaml → afterpython-0.1.0.dev5/afterpython/.pre-commit-config.yaml +1 -1
  4. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/_io/toml.py +3 -5
  5. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/_typing.py +2 -1
  6. afterpython-0.1.0.dev5/afterpython/builders/__init__.py +8 -0
  7. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/builders/metadata.py +3 -7
  8. afterpython-0.1.0.dev5/afterpython/builders/molab.py +99 -0
  9. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/__init__.py +1 -0
  10. afterpython-0.1.0.dev5/afterpython/cli/commands/build.py +190 -0
  11. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/bump.py +0 -30
  12. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/check.py +1 -1
  13. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/clean.py +3 -4
  14. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/commit.py +3 -21
  15. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/commitizen.py +1 -1
  16. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/dev.py +13 -80
  17. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/format.py +1 -1
  18. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/init.py +39 -48
  19. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/install.py +2 -8
  20. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/pre_commit.py +1 -1
  21. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/preview.py +1 -1
  22. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/start.py +4 -8
  23. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/sync.py +21 -65
  24. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/update.py +8 -31
  25. afterpython-0.1.0.dev5/afterpython/cli/main.py +71 -0
  26. afterpython-0.1.0.dev5/afterpython/const.py +5 -0
  27. afterpython-0.1.0.dev5/afterpython/cz.toml +27 -0
  28. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/pcu.py +1 -3
  29. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/templates/deploy-workflow-template.yml +7 -5
  30. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/templates/release-workflow-template.yml +3 -3
  31. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/templates/ruff-template.toml +1 -1
  32. afterpython-0.1.0.dev5/afterpython/tools/_afterpython.py +65 -0
  33. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/tools/_git.py +2 -28
  34. afterpython-0.1.0.dev5/afterpython/tools/commitizen.py +53 -0
  35. afterpython-0.1.0.dev5/afterpython/tools/github_actions.py +78 -0
  36. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/tools/myst.py +38 -82
  37. afterpython-0.1.0.dev5/afterpython/tools/pre_commit.py +62 -0
  38. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/tools/pyproject.py +2 -40
  39. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/utils.py +3 -87
  40. {afterpython-0.1.0 → afterpython-0.1.0.dev5}/pyproject.toml +43 -13
  41. afterpython-0.1.0/src/afterpython/builders/__init__.py +0 -17
  42. afterpython-0.1.0/src/afterpython/builders/content_json.py +0 -131
  43. afterpython-0.1.0/src/afterpython/builders/index_md.py +0 -94
  44. afterpython-0.1.0/src/afterpython/builders/jupyter_notebook.py +0 -293
  45. afterpython-0.1.0/src/afterpython/builders/markdown.py +0 -271
  46. afterpython-0.1.0/src/afterpython/cli/commands/add.py +0 -67
  47. afterpython-0.1.0/src/afterpython/cli/commands/build.py +0 -260
  48. afterpython-0.1.0/src/afterpython/cli/commands/init_branch_rules.py +0 -9
  49. afterpython-0.1.0/src/afterpython/cli/commands/lock.py +0 -23
  50. afterpython-0.1.0/src/afterpython/cli/commands/remove.py +0 -67
  51. afterpython-0.1.0/src/afterpython/cli/main.py +0 -132
  52. afterpython-0.1.0/src/afterpython/const.py +0 -4
  53. afterpython-0.1.0/src/afterpython/templates/ci-workflow-template.yml +0 -197
  54. afterpython-0.1.0/src/afterpython/templates/cz-template.toml +0 -7
  55. afterpython-0.1.0/src/afterpython/templates/dependabot-template.yml +0 -30
  56. afterpython-0.1.0/src/afterpython/tools/_afterpython.py +0 -128
  57. afterpython-0.1.0/src/afterpython/tools/branch_rules.py +0 -106
  58. afterpython-0.1.0/src/afterpython/tools/commitizen.py +0 -13
  59. afterpython-0.1.0/src/afterpython/tools/github_actions.py +0 -48
  60. afterpython-0.1.0/src/afterpython/tools/pre_commit.py +0 -37
  61. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/__init__.py +0 -0
  62. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/_io/yaml.py +0 -0
  63. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/_paths.py +1 -1
  64. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/cli/commands/release.py +0 -0
  65. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/main.py +0 -0
  66. {afterpython-0.1.0/src → afterpython-0.1.0.dev5}/afterpython/version.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: afterpython
3
- Version: 0.1.0
4
- Summary: CLI tool to streamline Python package maintenance and generate a dedicated project website
3
+ Version: 0.1.0.dev5
4
+ Summary: CLI tool for AfterPython
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
7
7
  Author-email: Stephen Yau <softwareentrepreneer+afterpython@gmail.com>
@@ -10,38 +10,39 @@ Classifier: Operating System :: OS Independent
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Topic :: Software Development :: Build Tools
12
12
  Classifier: Topic :: Software Development :: Documentation
13
- Classifier: Typing :: Typed
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
17
  Requires-Dist: click>=8.3.1
18
18
  Requires-Dist: trogon>=0.6.0
19
19
  Requires-Dist: httpx>=0.28.1
20
20
  Requires-Dist: tomlkit>=0.13.3
21
21
  Requires-Dist: ruamel-yaml>=0.18.16
22
22
  Requires-Dist: gitpython>=3.1.45
23
+ Requires-Dist: pygithub>=2.8.1
23
24
  Requires-Dist: python-dotenv>=1.2.1
24
- Requires-Dist: pyproject-metadata>=0.10.0
25
- Requires-Dist: mystmd>=1.7.0
25
+ Requires-Dist: pyproject-metadata>=0.9.1
26
+ Requires-Dist: mystmd>=1.6.6
26
27
  Requires-Dist: pdoc>=16.0.0
27
- Requires-Dist: ruff>=0.14.8
28
- Requires-Dist: pre-commit>=4.5.0
28
+ Requires-Dist: ruff>=0.14.5
29
+ Requires-Dist: pre-commit>=4.4.0
29
30
  Requires-Dist: commitizen>=4.10.0
30
31
  Requires-Python: >=3.11
31
- Project-URL: documentation, https://afterpython.afterpython.org/doc
32
- Project-URL: homepage, https://afterpython.afterpython.org
32
+ Project-URL: documentation, https://ap.afterpython.org/doc
33
+ Project-URL: homepage, https://afterpython.org
33
34
  Project-URL: repository, https://github.com/AfterPythonOrg/afterpython
34
35
  Description-Content-Type: text/markdown
35
36
 
36
37
  # AfterPython: Python Package Maintenance Toolkit and Project Website Generator
37
38
 
38
39
  [![afterpython](https://afterpython.org/shield.svg)](https://afterpython.org)
40
+ [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)
39
41
  ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython)
40
42
  [![PyPI](https://img.shields.io/pypi/v/afterpython.svg)](https://pypi.org/project/afterpython)
41
43
  ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython)
42
44
  [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
43
45
  [![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
 
46
47
 
47
48
  [MyST]: https://mystmd.org
@@ -56,7 +57,6 @@ Description-Content-Type: text/markdown
56
57
  [Scikit-Learn]: https://scikit-learn.org
57
58
  [WebLLM]: https://webllm.mlc.ai/
58
59
  [project-website-template]: https://github.com/AfterPythonOrg/project-website-template
59
- [commitizen]: https://github.com/commitizen-tools/commitizen
60
60
  [uv]: https://docs.astral.sh/uv/
61
61
  [ruff]: https://docs.astral.sh/ruff/
62
62
 
@@ -75,54 +75,28 @@ First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [Sem
75
75
 
76
76
  ## Core Features
77
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**
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
81
80
  - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
82
81
  - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
83
82
  - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
84
83
 
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
84
 
96
- ---
97
85
  ## Installation
98
86
  ```bash
99
87
  # install afterpython as a dev dependency
100
88
  uv add --dev afterpython
101
-
102
89
  # initialize afterpython
103
90
  ap init
104
91
  ```
105
92
 
106
93
 
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
94
  ## Tech Stack
120
95
  - [MyST]
121
96
  - [project-website-template]
122
97
  - [pre-commit]
123
98
  - [GitHub Actions]
124
99
  - [pdoc]
125
- - [commitizen]
126
100
  - [uv]
127
101
  - [ruff]
128
102
  - [pagefind]
@@ -1,12 +1,12 @@
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
+ [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)
4
5
  ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython)
5
6
  [![PyPI](https://img.shields.io/pypi/v/afterpython.svg)](https://pypi.org/project/afterpython)
6
7
  ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython)
7
8
  [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
8
9
  [![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
10
 
11
11
 
12
12
  [MyST]: https://mystmd.org
@@ -21,7 +21,6 @@
21
21
  [Scikit-Learn]: https://scikit-learn.org
22
22
  [WebLLM]: https://webllm.mlc.ai/
23
23
  [project-website-template]: https://github.com/AfterPythonOrg/project-website-template
24
- [commitizen]: https://github.com/commitizen-tools/commitizen
25
24
  [uv]: https://docs.astral.sh/uv/
26
25
  [ruff]: https://docs.astral.sh/ruff/
27
26
 
@@ -40,54 +39,28 @@ First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [Sem
40
39
 
41
40
  ## Core Features
42
41
  - [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**
42
+ - [x] Go from writing to website deployment in minutes — no need to learn any of the underlying tools
43
+ - [x] Centralize all your content in a modern, unified project website — from documentation to blog posts
46
44
  - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
47
45
  - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
48
46
  - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
49
47
 
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
48
 
61
- ---
62
49
  ## Installation
63
50
  ```bash
64
51
  # install afterpython as a dev dependency
65
52
  uv add --dev afterpython
66
-
67
53
  # initialize afterpython
68
54
  ap init
69
55
  ```
70
56
 
71
57
 
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
58
  ## Tech Stack
85
59
  - [MyST]
86
60
  - [project-website-template]
87
61
  - [pre-commit]
88
62
  - [GitHub Actions]
89
63
  - [pdoc]
90
- - [commitizen]
91
64
  - [uv]
92
65
  - [ruff]
93
66
  - [pagefind]
@@ -23,7 +23,7 @@ repos:
23
23
  stages:
24
24
  - pre-commit
25
25
  - repo: https://github.com/astral-sh/ruff-pre-commit
26
- rev: v0.14.8
26
+ rev: v0.14.5
27
27
  hooks:
28
28
  - id: ruff-check
29
29
  stages:
@@ -1,11 +1,9 @@
1
1
  from __future__ import annotations
2
-
3
2
  from typing import TYPE_CHECKING
4
3
 
5
4
  if TYPE_CHECKING:
6
- from pathlib import Path
7
-
8
5
  from tomlkit.toml_document import TOMLDocument
6
+ from pathlib import Path
9
7
 
10
8
  import tomlkit
11
9
 
@@ -18,7 +16,7 @@ def _from_tomlkit(value):
18
16
  - tomlkit containers → plain Python containers
19
17
  - tomlkit primitives → plain Python primitives
20
18
  """
21
- from tomlkit.items import AoT, Array, InlineTable, Table
19
+ from tomlkit.items import Array, InlineTable, Table, AoT
22
20
 
23
21
  if value is None:
24
22
  return None
@@ -60,7 +58,7 @@ def _to_tomlkit(value):
60
58
  - List of dicts → array of inline tables: [{key = val}]
61
59
  - Nested dicts → nested tables with proper structure
62
60
  """
63
- from tomlkit import array, inline_table
61
+ from tomlkit import inline_table, array
64
62
 
65
63
  if value is None:
66
64
  return None
@@ -1,4 +1,5 @@
1
- from typing import Literal, TypeAlias
1
+ from typing import TypeAlias, Literal
2
+
2
3
 
3
4
  NodeEnv: TypeAlias = dict[str, str]
4
5
  tContentType = Literal["doc", "blog", "tutorial", "example", "guide"]
@@ -0,0 +1,8 @@
1
+ from afterpython.builders.metadata import build_metadata
2
+ from afterpython.builders.molab import add_molab_badge_to_jupyter_notebooks
3
+
4
+
5
+ __all__ = (
6
+ "add_molab_badge_to_jupyter_notebooks",
7
+ "build_metadata",
8
+ )
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
-
3
2
  from typing import TYPE_CHECKING
4
3
 
5
4
  if TYPE_CHECKING:
@@ -7,10 +6,9 @@ if TYPE_CHECKING:
7
6
 
8
7
  import json
9
8
 
10
- import click
11
-
12
9
  import afterpython as ap
13
10
 
11
+
14
12
  build_path = ap.paths.build_path
15
13
 
16
14
 
@@ -25,22 +23,20 @@ def convert_paths():
25
23
  markdown_text = metadata["description"]
26
24
 
27
25
  # Replace with the correct paths
28
- updated_markdown = markdown_text.replace("afterpython/static/", "/")
26
+ updated_markdown = markdown_text.replace("./afterpython/static/", "/")
29
27
 
30
28
  # Write back to metadata.json
31
29
  metadata["description"] = updated_markdown
32
30
  with open(build_path / "metadata.json", "w") as f:
33
31
  json.dump(metadata, f, indent=2)
34
32
 
35
- click.echo("Completed path conversion in metadata.json")
33
+ print("Completed path conversion in metadata.json")
36
34
 
37
35
 
38
36
  def build_metadata():
39
37
  """Build metadata.json using pyproject.toml"""
40
38
  from afterpython.tools.pyproject import read_metadata
41
39
 
42
- click.echo("Building metadata.json...")
43
-
44
40
  metadata: StandardMetadata = read_metadata()
45
41
 
46
42
  # Write to metadata.json
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from afterpython._typing import tContentType
6
+
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from pyproject_metadata import StandardMetadata
11
+
12
+ import afterpython as ap
13
+ from afterpython.tools.pyproject import read_metadata
14
+ from afterpython.const import CONTENT_TYPES
15
+
16
+
17
+ def _get_molab_badge() -> str:
18
+ return "https://marimo.io/molab-shield.svg"
19
+
20
+
21
+ def _create_molab_url(github_url: str, content_path: Path):
22
+ """Create a molab URL for a given content type and notebook path.
23
+ Args:
24
+ github_url: str, e.g. "https://github.com/AfterPythonOrg/afterpython"
25
+ content_path: str, e.g. "tutorial/test.ipynb"
26
+ """
27
+ github_url = github_url.replace("https://github.com/", "github/")
28
+ return f"https://molab.marimo.io/{github_url}/blob/main/afterpython/{content_path.as_posix()}"
29
+
30
+
31
+ def add_molab_badge_to_jupyter_notebooks(content_type: tContentType):
32
+ """Add a badge markdown cell to the top of Jupyter notebooks in the given content type directory, e.g. tutorial/, blog/, etc.
33
+ # NOTE
34
+ Note that the jupyter notebooks need to exist in the github repository first.
35
+ If you have renamed a notebook, you need to push the changes to the github repository first for the badge to work.
36
+ """
37
+ assert content_type.lower() in CONTENT_TYPES, (
38
+ f"Invalid content type: {content_type}"
39
+ )
40
+ metadata: StandardMetadata = read_metadata()
41
+
42
+ if "repository" not in metadata.urls:
43
+ return "Repository URL not found in [project.urls] in pyproject.toml, cannot add molab badge"
44
+ else:
45
+ github_url = metadata.urls["repository"]
46
+
47
+ path = ap.paths.afterpython_path / content_type.lower()
48
+
49
+ # iterate over all files in the path
50
+ for notebook_path in path.rglob("*.ipynb"):
51
+ # Skip files in _build directory
52
+ if "_build" in notebook_path.parts:
53
+ continue
54
+
55
+ # Get path relative to afterpython/ (includes tutorial/ in the path)
56
+ content_path = notebook_path.relative_to(ap.paths.afterpython_path)
57
+ molab_url = _create_molab_url(github_url, content_path)
58
+ badge_md = f"[![Open in molab]({_get_molab_badge()})]({molab_url})"
59
+
60
+ # Read the notebook
61
+ try:
62
+ # Read the notebook
63
+ with open(notebook_path) as f:
64
+ notebook = json.load(f)
65
+ except json.JSONDecodeError:
66
+ print(f"✗ Skipping {content_path} - invalid or empty JSON")
67
+ continue
68
+ except Exception as e:
69
+ print(f"✗ Error reading {content_path}: {e}")
70
+ continue
71
+
72
+ # Validate notebook structure
73
+ if "cells" not in notebook:
74
+ print(f"✗ Skipping {content_path} - not a valid Jupyter notebook")
75
+ continue
76
+
77
+ # Create a markdown cell with the badge
78
+ badge_cell = {
79
+ "cell_type": "markdown",
80
+ "id": "molab-badge-cell", # Unique ID for the badge cell
81
+ "metadata": {
82
+ "tags": ["molab", "hide-input"] # Optional: use MyST tags
83
+ },
84
+ "source": [badge_md],
85
+ }
86
+
87
+ # Check if first cell is already a badge cell (avoid duplicates)
88
+ if notebook["cells"] and notebook["cells"][0].get("id") == "molab-badge-cell":
89
+ # Update existing badge
90
+ notebook["cells"][0] = badge_cell
91
+ print(f"✓ Updated molab badge in: {content_path}")
92
+ else:
93
+ # Insert at the beginning
94
+ notebook["cells"].insert(0, badge_cell)
95
+ print(f"✓ Added molab badge to: {content_path}")
96
+
97
+ # Write back
98
+ with open(notebook_path, "w") as f:
99
+ json.dump(notebook, f, indent=1)
@@ -1,3 +1,4 @@
1
1
  from afterpython.cli.main import afterpython_group
2
2
 
3
+
3
4
  __all__ = ["afterpython_group"]
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+ from collections.abc import Callable
4
+
5
+ if TYPE_CHECKING:
6
+ from afterpython._typing import NodeEnv
7
+ from pathlib import Path
8
+
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+
13
+ import click
14
+ from click.exceptions import Exit
15
+
16
+ import afterpython as ap
17
+ from afterpython.utils import find_node_env
18
+ from afterpython.const import CONTENT_TYPES
19
+ from afterpython.builders import (
20
+ build_metadata,
21
+ add_molab_badge_to_jupyter_notebooks,
22
+ )
23
+
24
+
25
+ def prebuild():
26
+ def _check_initialized():
27
+ # Check if 'ap init' has been run
28
+ afterpython_toml = ap.paths.afterpython_path / "afterpython.toml"
29
+ if not afterpython_toml.exists():
30
+ raise click.ClickException(
31
+ "AfterPython is not initialized!\n"
32
+ "Run 'ap init' first to set up your project."
33
+ )
34
+
35
+ def _clean_build_directory():
36
+ print("Cleaning up build directory...")
37
+ build_path = ap.paths.build_path
38
+ if build_path.exists():
39
+ shutil.rmtree(build_path)
40
+ build_path.mkdir(parents=True, exist_ok=True)
41
+
42
+ _check_initialized()
43
+ _clean_build_directory()
44
+
45
+ if os.getenv("AP_MOLAB_BADGE", "0") == "1":
46
+ for content_type in CONTENT_TYPES:
47
+ add_molab_badge_to_jupyter_notebooks(content_type)
48
+
49
+
50
+ def postbuild():
51
+ def _move_files(
52
+ source: Path,
53
+ destination: Path,
54
+ is_copy: bool = True,
55
+ ignore_copy: Callable[[str, list[str]], set[str]] | None = None,
56
+ ):
57
+ """
58
+ Move or copy files from source to destination.
59
+
60
+ Args:
61
+ source: Source path
62
+ destination: Destination path
63
+ is_copy: If True, copy files (merge with existing). If False, move files (replace destination)
64
+ ignore_copy: Optional ignore function for copytree, takes (directory, contents) and returns set of names to ignore
65
+ """
66
+ if not source.exists():
67
+ return
68
+ if is_copy:
69
+ shutil.copytree(source, destination, dirs_exist_ok=True, ignore=ignore_copy)
70
+ print(f"Copied: {source} to {destination}")
71
+ else:
72
+ # Remove destination if it exists (move will fail otherwise)
73
+ if destination.exists():
74
+ shutil.rmtree(destination)
75
+ shutil.move(str(source), str(destination))
76
+ print(f"Moved: {source} to {destination}")
77
+
78
+ website_static = ap.paths.website_path / "static"
79
+ website_static.mkdir(parents=True, exist_ok=True)
80
+
81
+ for content_type in CONTENT_TYPES:
82
+ myst_build = ap.paths.afterpython_path / content_type / "_build"
83
+ afterpython_content_build = ap.paths.build_path / content_type
84
+ # Move myst builds from afterpython/{content_type}/_build to afterpython/_build/{content_type}
85
+ _move_files(myst_build, afterpython_content_build, is_copy=False)
86
+ # Copy afterpython/_build/{content_type}/html to afterpython/_website/static/{content_type}
87
+ _move_files(
88
+ afterpython_content_build / "html",
89
+ website_static / content_type,
90
+ is_copy=True,
91
+ )
92
+
93
+ # Copy all files from afterpython/_build to afterpython/_website/static/
94
+ _move_files(
95
+ ap.paths.build_path,
96
+ website_static,
97
+ is_copy=True,
98
+ # ignore all content builds e.g. doc/, blog/, only their html/ files will be copied
99
+ ignore_copy=lambda dir, contents: [
100
+ name for name in contents if name in CONTENT_TYPES
101
+ ],
102
+ )
103
+
104
+ # Copy all static files from afterpython/static/ to afterpython/_website/static/
105
+ _move_files(ap.paths.static_path, website_static, is_copy=True)
106
+
107
+
108
+ @click.command(
109
+ add_help_option=False, # disable click's --help option so that ap build --help can work
110
+ context_settings=dict(
111
+ ignore_unknown_options=True,
112
+ allow_extra_args=True,
113
+ ),
114
+ )
115
+ @click.pass_context
116
+ @click.option(
117
+ "--dev",
118
+ is_flag=True,
119
+ hidden=True, # Internal flag used by `ap dev`, not exposed to users
120
+ help="Development build - only build metadata for the landing page, skip content and production builds",
121
+ )
122
+ @click.option(
123
+ "--execute", is_flag=True, help="Execute Jupyter notebooks for all content types"
124
+ )
125
+ def build(ctx, dev: bool, execute: bool):
126
+ """Build the project website and all contents for production.
127
+
128
+ This command builds MyST content (doc/blog/tutorial/example/guide) and the SvelteKit website.
129
+
130
+ Any extra arguments are passed to the 'myst build --html' command for each content type.
131
+ See "myst build --help" for more details.
132
+
133
+ Use --execute to execute Jupyter notebooks for all content types.
134
+ """
135
+ from afterpython.utils import has_content_for_myst
136
+ from afterpython.utils import handle_passthrough_help
137
+
138
+ # Show both our options and myst's help and exit
139
+ handle_passthrough_help(
140
+ ctx,
141
+ ["myst", "build"],
142
+ show_underlying=True,
143
+ )
144
+
145
+ paths = ctx.obj["paths"]
146
+ prebuild()
147
+
148
+ click.echo("Building metadata.json...")
149
+ build_metadata()
150
+
151
+ # myst's production build
152
+ if not dev:
153
+ node_env: NodeEnv = find_node_env()
154
+ for content_type in CONTENT_TYPES:
155
+ content_path = paths.afterpython_path / content_type
156
+ if not has_content_for_myst(content_path):
157
+ click.echo(f"Skipping {content_type}/ (no content files found)")
158
+ continue
159
+
160
+ click.echo(f"Building {content_type}/...")
161
+ # NOTE: needs to set BASE_URL so that the project website can link to the content pages correctly at e.g. localhost:5173/doc
162
+ # BASE_PATH is set by the GitHub Actions workflow
163
+ base_path = os.getenv("BASE_PATH", "")
164
+ base_url = f"{base_path}/{content_type}"
165
+ build_env = {**node_env, "BASE_URL": base_url}
166
+ result = subprocess.run(
167
+ [
168
+ "myst",
169
+ "build",
170
+ "--html",
171
+ *(["--execute"] if execute else []),
172
+ *ctx.args,
173
+ ],
174
+ cwd=content_path,
175
+ env=build_env,
176
+ check=False,
177
+ )
178
+ if result.returncode != 0:
179
+ raise Exit(result.returncode)
180
+
181
+ postbuild()
182
+
183
+ # website's production build
184
+ if not dev:
185
+ click.echo("Building project website...")
186
+ result = subprocess.run(
187
+ ["pnpm", "build"], cwd=paths.website_path, env=node_env, check=False
188
+ )
189
+ if result.returncode != 0:
190
+ raise Exit(result.returncode)
@@ -1,9 +1,6 @@
1
1
  import subprocess
2
2
 
3
3
  import click
4
- from click.exceptions import Exit
5
-
6
- from afterpython.utils import has_pixi
7
4
 
8
5
 
9
6
  @click.command(
@@ -89,30 +86,3 @@ def bump(ctx, release: bool, pre: bool):
89
86
  args = ["--prerelease", prerelease_type, *ctx.args]
90
87
 
91
88
  subprocess.run(["ap", "cz", "bump", *args])
92
-
93
- # Update pixi.lock after version bump (editable install requires lock refresh)
94
- if has_pixi():
95
- result = subprocess.run(["pixi", "install"], check=False)
96
- if result.returncode != 0:
97
- raise Exit(result.returncode)
98
-
99
- # Check if pixi.lock was modified
100
- status = subprocess.run(
101
- ["git", "diff", "--name-only", "pixi.lock"],
102
- capture_output=True,
103
- text=True,
104
- check=False,
105
- )
106
- if status.stdout.strip():
107
- # Stage and commit pixi.lock
108
- subprocess.run(["git", "add", "pixi.lock"], check=True)
109
- subprocess.run(
110
- [
111
- "git",
112
- "commit",
113
- "-m",
114
- "build: update pixi.lock after version bump",
115
- ],
116
- check=True,
117
- )
118
- click.echo("✓ Committed pixi.lock update")
@@ -13,7 +13,7 @@ from click.exceptions import Exit
13
13
  )
14
14
  @click.pass_context
15
15
  def check(ctx):
16
- """Run ruff linter (uses afterpython/ruff.toml if available)"""
16
+ """Simple wrapper for ruff check for convenience, always use the afterpython/ruff.toml if it exists"""
17
17
  from afterpython.utils import handle_passthrough_help
18
18
 
19
19
  # Show both our options and ruff's help and exit
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
-
3
2
  from typing import TYPE_CHECKING
4
3
 
5
4
  if TYPE_CHECKING:
@@ -11,8 +10,8 @@ import subprocess
11
10
  import click
12
11
  from click.exceptions import Exit
13
12
 
14
- from afterpython.const import CONTENT_TYPES
15
13
  from afterpython.utils import find_node_env, has_content_for_myst
14
+ from afterpython.const import CONTENT_TYPES
16
15
 
17
16
 
18
17
  @click.command(
@@ -26,10 +25,10 @@ from afterpython.utils import find_node_env, has_content_for_myst
26
25
  @click.option(
27
26
  "--all",
28
27
  is_flag=True,
29
- help="Also clean AfterPython build directories (_build/ and _website/build/)",
28
+ help='--all passed to "myst clean", also clean afterpython build directories (_build/ and _website/build/)',
30
29
  )
31
30
  def clean(ctx, all: bool):
32
- """Clean build artifacts from content types and website"""
31
+ """Clean the build directory"""
33
32
  from afterpython.utils import handle_passthrough_help
34
33
 
35
34
  # Show both our options and myst's help and exit