afterpython 0.0.1.dev2__tar.gz → 0.1.0.dev4__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 (55) hide show
  1. afterpython-0.1.0.dev4/PKG-INFO +105 -0
  2. afterpython-0.1.0.dev4/README.md +69 -0
  3. afterpython-0.1.0.dev4/afterpython/.pre-commit-config.yaml +48 -0
  4. afterpython-0.1.0.dev4/afterpython/__init__.py +13 -0
  5. afterpython-0.1.0.dev4/afterpython/_io/toml.py +103 -0
  6. afterpython-0.1.0.dev4/afterpython/_io/yaml.py +26 -0
  7. afterpython-0.1.0.dev4/afterpython/_paths.py +39 -0
  8. afterpython-0.1.0.dev4/afterpython/_typing.py +5 -0
  9. afterpython-0.1.0.dev4/afterpython/builders/__init__.py +8 -0
  10. afterpython-0.1.0.dev4/afterpython/builders/metadata.py +46 -0
  11. afterpython-0.1.0.dev4/afterpython/builders/molab.py +99 -0
  12. afterpython-0.1.0.dev4/afterpython/cli/__init__.py +4 -0
  13. afterpython-0.1.0.dev4/afterpython/cli/commands/build.py +187 -0
  14. afterpython-0.1.0.dev4/afterpython/cli/commands/bump.py +88 -0
  15. afterpython-0.1.0.dev4/afterpython/cli/commands/check.py +36 -0
  16. afterpython-0.1.0.dev4/afterpython/cli/commands/clean.py +84 -0
  17. afterpython-0.1.0.dev4/afterpython/cli/commands/commit.py +34 -0
  18. afterpython-0.1.0.dev4/afterpython/cli/commands/commitizen.py +32 -0
  19. afterpython-0.1.0.dev4/afterpython/cli/commands/dev.py +146 -0
  20. afterpython-0.1.0.dev4/afterpython/cli/commands/format.py +28 -0
  21. afterpython-0.1.0.dev4/afterpython/cli/commands/init.py +98 -0
  22. afterpython-0.1.0.dev4/afterpython/cli/commands/install.py +22 -0
  23. afterpython-0.1.0.dev4/afterpython/cli/commands/pre_commit.py +66 -0
  24. afterpython-0.1.0.dev4/afterpython/cli/commands/preview.py +22 -0
  25. afterpython-0.1.0.dev4/afterpython/cli/commands/release.py +67 -0
  26. afterpython-0.1.0.dev4/afterpython/cli/commands/start.py +140 -0
  27. afterpython-0.1.0.dev4/afterpython/cli/commands/sync.py +108 -0
  28. afterpython-0.1.0.dev4/afterpython/cli/commands/update.py +153 -0
  29. afterpython-0.1.0.dev4/afterpython/cli/main.py +71 -0
  30. afterpython-0.1.0.dev4/afterpython/const.py +5 -0
  31. afterpython-0.1.0.dev4/afterpython/cz.toml +27 -0
  32. afterpython-0.1.0.dev4/afterpython/main.py +25 -0
  33. afterpython-0.1.0.dev4/afterpython/pcu.py +164 -0
  34. afterpython-0.1.0.dev4/afterpython/templates/deploy-workflow-template.yml +71 -0
  35. afterpython-0.1.0.dev4/afterpython/templates/release-workflow-template.yml +56 -0
  36. afterpython-0.1.0.dev4/afterpython/templates/ruff-template.toml +32 -0
  37. afterpython-0.1.0.dev4/afterpython/tools/_afterpython.py +65 -0
  38. afterpython-0.1.0.dev4/afterpython/tools/_git.py +111 -0
  39. afterpython-0.1.0.dev4/afterpython/tools/commitizen.py +53 -0
  40. afterpython-0.1.0.dev4/afterpython/tools/github_actions.py +78 -0
  41. afterpython-0.1.0.dev4/afterpython/tools/myst.py +164 -0
  42. afterpython-0.1.0.dev4/afterpython/tools/pre_commit.py +62 -0
  43. afterpython-0.1.0.dev4/afterpython/tools/pyproject.py +72 -0
  44. afterpython-0.1.0.dev4/afterpython/utils.py +215 -0
  45. afterpython-0.1.0.dev4/afterpython/version.py +39 -0
  46. afterpython-0.1.0.dev4/pyproject.toml +103 -0
  47. afterpython-0.0.1.dev2/LICENSE +0 -201
  48. afterpython-0.0.1.dev2/PKG-INFO +0 -20
  49. afterpython-0.0.1.dev2/README.md +0 -1
  50. afterpython-0.0.1.dev2/afterpython/README.py +0 -158
  51. afterpython-0.0.1.dev2/afterpython/images/favicon.ico +0 -0
  52. afterpython-0.0.1.dev2/afterpython/images/logo-trace.svg +0 -3
  53. afterpython-0.0.1.dev2/afterpython/images/logo.png +0 -0
  54. afterpython-0.0.1.dev2/afterpython/images/logo.svg +0 -2
  55. afterpython-0.0.1.dev2/pyproject.toml +0 -14
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: afterpython
3
+ Version: 0.1.0.dev4
4
+ Summary: CLI tool for AfterPython
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: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
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: pygithub>=2.8.1
24
+ Requires-Dist: python-dotenv>=1.2.1
25
+ Requires-Dist: pyproject-metadata>=0.9.1
26
+ Requires-Dist: mystmd>=1.6.6
27
+ Requires-Dist: pdoc>=16.0.0
28
+ Requires-Dist: ruff>=0.14.5
29
+ Requires-Dist: pre-commit>=4.4.0
30
+ Requires-Dist: commitizen>=4.10.0
31
+ Requires-Python: >=3.11
32
+ Project-URL: documentation, https://docs.afterpython.org
33
+ Project-URL: homepage, https://afterpython.org
34
+ Project-URL: repository, https://github.com/AfterPythonOrg/afterpython
35
+ Description-Content-Type: text/markdown
36
+
37
+ # AfterPython: Python Package Maintenance Toolkit and Project Website Generator
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)
41
+ ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython)
42
+ [![PyPI](https://img.shields.io/pypi/v/afterpython.svg)](https://pypi.org/project/afterpython)
43
+ ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython)
44
+ [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
45
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/AfterPythonOrg/afterpython)
46
+
47
+
48
+ [MyST]: https://mystmd.org
49
+ [MyST Markdown]: https://mystmd.org/spec/
50
+ [Jupyter Notebook]: https://jupyter.org
51
+ [pre-commit]: https://pre-commit.com
52
+ [pagefind]: https://pagefind.app
53
+ [SemVer]: https://semver.org
54
+ [pdoc]: https://pdoc.dev/
55
+ [GitHub Actions]: https://github.com/features/actions
56
+ [PyTorch]: https://pytorch.org
57
+ [Scikit-Learn]: https://scikit-learn.org
58
+ [WebLLM]: https://webllm.mlc.ai/
59
+ [project-website-template]: https://github.com/AfterPythonOrg/project-website-template
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
+ - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
81
+ - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
82
+ - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
83
+
84
+
85
+ ## Installation
86
+ ```bash
87
+ # install afterpython as a dev dependency
88
+ uv add --dev afterpython
89
+ # initialize afterpython
90
+ ap init
91
+ ```
92
+
93
+
94
+ ## Tech Stack
95
+ - [MyST]
96
+ - [project-website-template]
97
+ - [pre-commit]
98
+ - [GitHub Actions]
99
+ - [pdoc]
100
+ - [uv]
101
+ - [ruff]
102
+ - [pagefind]
103
+ - [WebLLM]
104
+ <!-- - ty -->
105
+ <!-- - [pixi] -->
@@ -0,0 +1,69 @@
1
+ # AfterPython: Python Package Maintenance Toolkit and Project Website Generator
2
+
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)
5
+ ![PyPI downloads](https://img.shields.io/pypi/dm/afterpython)
6
+ [![PyPI](https://img.shields.io/pypi/v/afterpython.svg)](https://pypi.org/project/afterpython)
7
+ ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/afterpython)
8
+ [![Discussions](https://img.shields.io/badge/Discussions-Let's%20Chat-green)](https://github.com/AfterPythonOrg/afterpython/discussions)
9
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/AfterPythonOrg/afterpython)
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
+ [uv]: https://docs.astral.sh/uv/
25
+ [ruff]: https://docs.astral.sh/ruff/
26
+
27
+
28
+ ## Problem
29
+ Going from **writing Python code to publishing and maintaining a package** is **time-consuming**.
30
+ 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.
31
+
32
+
33
+ ## Solution
34
+ `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.
35
+
36
+ ---
37
+ `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.
38
+
39
+
40
+ ## Core Features
41
+ - [x] Write content directly in [MyST Markdown] or [Jupyter Notebook]
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
44
+ - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
45
+ - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
46
+ - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
47
+
48
+
49
+ ## Installation
50
+ ```bash
51
+ # install afterpython as a dev dependency
52
+ uv add --dev afterpython
53
+ # initialize afterpython
54
+ ap init
55
+ ```
56
+
57
+
58
+ ## Tech Stack
59
+ - [MyST]
60
+ - [project-website-template]
61
+ - [pre-commit]
62
+ - [GitHub Actions]
63
+ - [pdoc]
64
+ - [uv]
65
+ - [ruff]
66
+ - [pagefind]
67
+ - [WebLLM]
68
+ <!-- - ty -->
69
+ <!-- - [pixi] -->
@@ -0,0 +1,48 @@
1
+ default_install_hook_types:
2
+ - pre-commit
3
+ - commit-msg
4
+ - pre-push
5
+ exclude: ^afterpython/_website/
6
+ repos:
7
+ - repo: https://github.com/pre-commit/pre-commit-hooks
8
+ rev: v6.0.0
9
+ hooks:
10
+ - id: trailing-whitespace
11
+ stages:
12
+ - pre-commit
13
+ - id: end-of-file-fixer
14
+ stages:
15
+ - pre-commit
16
+ - id: check-yaml
17
+ stages:
18
+ - pre-commit
19
+ - id: check-toml
20
+ stages:
21
+ - pre-commit
22
+ - id: check-added-large-files
23
+ stages:
24
+ - pre-commit
25
+ - repo: https://github.com/astral-sh/ruff-pre-commit
26
+ rev: v0.14.5
27
+ hooks:
28
+ - id: ruff-check
29
+ stages:
30
+ - pre-commit
31
+ args:
32
+ - --config
33
+ - ./afterpython/ruff.toml
34
+ - id: ruff-format
35
+ stages:
36
+ - pre-commit
37
+ args:
38
+ - --config
39
+ - ./afterpython/ruff.toml
40
+ - repo: https://github.com/commitizen-tools/commitizen
41
+ rev: v4.10.0
42
+ hooks:
43
+ - id: commitizen
44
+ stages:
45
+ - commit-msg
46
+ - id: commitizen-branch
47
+ stages:
48
+ - pre-push
@@ -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,103 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from tomlkit.toml_document import TOMLDocument
6
+ from pathlib import Path
7
+
8
+ import tomlkit
9
+
10
+
11
+ # VIBE-CODED
12
+ def _from_tomlkit(value):
13
+ """Recursively convert tomlkit objects to plain Python data structures.
14
+
15
+ This ensures compatibility with other libraries (like ruamel.yaml):
16
+ - tomlkit containers → plain Python containers
17
+ - tomlkit primitives → plain Python primitives
18
+ """
19
+ from tomlkit.items import Array, InlineTable, Table, AoT
20
+
21
+ if value is None:
22
+ return None
23
+
24
+ # Check containers first (these are the problematic ones)
25
+ if isinstance(value, (Table, InlineTable)):
26
+ # tomlkit dict-like → plain dict
27
+ return {k: _from_tomlkit(v) for k, v in value.items()}
28
+ elif isinstance(value, (Array, AoT)):
29
+ # tomlkit array → plain list
30
+ return [_from_tomlkit(item) for item in value]
31
+ elif isinstance(value, dict):
32
+ # Already a dict, but recurse to handle nested tomlkit objects
33
+ return {k: _from_tomlkit(v) for k, v in value.items()}
34
+ elif isinstance(value, list):
35
+ # Already a list, but recurse to handle nested tomlkit objects
36
+ return [_from_tomlkit(item) for item in value]
37
+
38
+ # Primitives - tomlkit primitives inherit from Python types
39
+ # Just convert explicitly to be safe
40
+ elif isinstance(value, bool): # MUST check bool before int!
41
+ return bool(value)
42
+ elif isinstance(value, int):
43
+ return int(value)
44
+ elif isinstance(value, float):
45
+ return float(value)
46
+ elif isinstance(value, str):
47
+ return str(value)
48
+ else:
49
+ # datetime, date, time, or already plain Python type
50
+ return value
51
+
52
+
53
+ # VIBE-CODED
54
+ def _to_tomlkit(value):
55
+ """Recursively convert Python data structures to tomlkit objects.
56
+
57
+ This ensures proper TOML formatting:
58
+ - List of dicts → array of inline tables: [{key = val}]
59
+ - Nested dicts → nested tables with proper structure
60
+ """
61
+ from tomlkit import inline_table, array
62
+
63
+ if value is None:
64
+ return None
65
+
66
+ if isinstance(value, dict):
67
+ # Plain dict → recursively convert values
68
+ return {k: _to_tomlkit(v) for k, v in value.items()}
69
+ elif isinstance(value, list):
70
+ if not value:
71
+ # Empty list → empty tomlkit array
72
+ return array()
73
+
74
+ # Check if ALL items are dicts (should be inline tables)
75
+ if all(isinstance(item, dict) for item in value):
76
+ arr = array()
77
+ for item in value:
78
+ tbl = inline_table()
79
+ # Recursively convert the dict values
80
+ tbl.update({k: _to_tomlkit(v) for k, v in item.items()})
81
+ arr.append(tbl)
82
+ return arr
83
+ else:
84
+ # Mixed or non-dict items → regular array
85
+ arr = array()
86
+ for item in value:
87
+ arr.append(_to_tomlkit(item))
88
+ return arr
89
+ else:
90
+ # Primitives (str, int, float, bool, datetime, etc.)
91
+ # tomlkit handles these natively
92
+ return value
93
+
94
+
95
+ def read_toml(file_path: Path) -> TOMLDocument:
96
+ with open(file_path, "rb") as f:
97
+ data: TOMLDocument = tomlkit.parse(f.read())
98
+ return data
99
+
100
+
101
+ def write_toml(file_path: Path, data: TOMLDocument | dict):
102
+ with open(file_path, "w") as f:
103
+ 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 pathlib import Path
2
+ from dataclasses import dataclass, field
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,5 @@
1
+ from typing import TypeAlias, Literal
2
+
3
+
4
+ NodeEnv: TypeAlias = dict[str, str]
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
+ )
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from pyproject_metadata import StandardMetadata
6
+
7
+ import json
8
+
9
+ import afterpython as ap
10
+
11
+
12
+ build_path = ap.paths.build_path
13
+
14
+
15
+ def convert_paths():
16
+ """
17
+ Convert paths in "description" field (README.md) in metadata.json to use the new paths in the build output.
18
+ e.g. convert ./afterpython/static/image.png to static/image.png
19
+ """
20
+ # Read metadata.json
21
+ with open(build_path / "metadata.json") as f:
22
+ metadata = json.load(f)
23
+ markdown_text = metadata["description"]
24
+
25
+ # Replace with the correct paths
26
+ updated_markdown = markdown_text.replace("./afterpython/static/", "/")
27
+
28
+ # Write back to metadata.json
29
+ metadata["description"] = updated_markdown
30
+ with open(build_path / "metadata.json", "w") as f:
31
+ json.dump(metadata, f, indent=2)
32
+
33
+ print("Completed path conversion in metadata.json")
34
+
35
+
36
+ def build_metadata():
37
+ """Build metadata.json using pyproject.toml"""
38
+ from afterpython.tools.pyproject import read_metadata
39
+
40
+ metadata: StandardMetadata = read_metadata()
41
+
42
+ # Write to metadata.json
43
+ with open(build_path / "metadata.json", "w") as f:
44
+ json.dump(metadata.as_json(), f, indent=2)
45
+
46
+ convert_paths()
@@ -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)
@@ -0,0 +1,4 @@
1
+ from afterpython.cli.main import afterpython_group
2
+
3
+
4
+ __all__ = ["afterpython_group"]