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.
- afterpython-0.1.0.dev4/PKG-INFO +105 -0
- afterpython-0.1.0.dev4/README.md +69 -0
- afterpython-0.1.0.dev4/afterpython/.pre-commit-config.yaml +48 -0
- afterpython-0.1.0.dev4/afterpython/__init__.py +13 -0
- afterpython-0.1.0.dev4/afterpython/_io/toml.py +103 -0
- afterpython-0.1.0.dev4/afterpython/_io/yaml.py +26 -0
- afterpython-0.1.0.dev4/afterpython/_paths.py +39 -0
- afterpython-0.1.0.dev4/afterpython/_typing.py +5 -0
- afterpython-0.1.0.dev4/afterpython/builders/__init__.py +8 -0
- afterpython-0.1.0.dev4/afterpython/builders/metadata.py +46 -0
- afterpython-0.1.0.dev4/afterpython/builders/molab.py +99 -0
- afterpython-0.1.0.dev4/afterpython/cli/__init__.py +4 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/build.py +187 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/bump.py +88 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/check.py +36 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/clean.py +84 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/commit.py +34 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/commitizen.py +32 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/dev.py +146 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/format.py +28 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/init.py +98 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/install.py +22 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/pre_commit.py +66 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/preview.py +22 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/release.py +67 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/start.py +140 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/sync.py +108 -0
- afterpython-0.1.0.dev4/afterpython/cli/commands/update.py +153 -0
- afterpython-0.1.0.dev4/afterpython/cli/main.py +71 -0
- afterpython-0.1.0.dev4/afterpython/const.py +5 -0
- afterpython-0.1.0.dev4/afterpython/cz.toml +27 -0
- afterpython-0.1.0.dev4/afterpython/main.py +25 -0
- afterpython-0.1.0.dev4/afterpython/pcu.py +164 -0
- afterpython-0.1.0.dev4/afterpython/templates/deploy-workflow-template.yml +71 -0
- afterpython-0.1.0.dev4/afterpython/templates/release-workflow-template.yml +56 -0
- afterpython-0.1.0.dev4/afterpython/templates/ruff-template.toml +32 -0
- afterpython-0.1.0.dev4/afterpython/tools/_afterpython.py +65 -0
- afterpython-0.1.0.dev4/afterpython/tools/_git.py +111 -0
- afterpython-0.1.0.dev4/afterpython/tools/commitizen.py +53 -0
- afterpython-0.1.0.dev4/afterpython/tools/github_actions.py +78 -0
- afterpython-0.1.0.dev4/afterpython/tools/myst.py +164 -0
- afterpython-0.1.0.dev4/afterpython/tools/pre_commit.py +62 -0
- afterpython-0.1.0.dev4/afterpython/tools/pyproject.py +72 -0
- afterpython-0.1.0.dev4/afterpython/utils.py +215 -0
- afterpython-0.1.0.dev4/afterpython/version.py +39 -0
- afterpython-0.1.0.dev4/pyproject.toml +103 -0
- afterpython-0.0.1.dev2/LICENSE +0 -201
- afterpython-0.0.1.dev2/PKG-INFO +0 -20
- afterpython-0.0.1.dev2/README.md +0 -1
- afterpython-0.0.1.dev2/afterpython/README.py +0 -158
- afterpython-0.0.1.dev2/afterpython/images/favicon.ico +0 -0
- afterpython-0.0.1.dev2/afterpython/images/logo-trace.svg +0 -3
- afterpython-0.0.1.dev2/afterpython/images/logo.png +0 -0
- afterpython-0.0.1.dev2/afterpython/images/logo.svg +0 -2
- 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
|
+
[](https://afterpython.org)
|
|
40
|
+
[](https://pixi.sh)
|
|
41
|
+

|
|
42
|
+
[](https://pypi.org/project/afterpython)
|
|
43
|
+

|
|
44
|
+
[](https://github.com/AfterPythonOrg/afterpython/discussions)
|
|
45
|
+
[](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
|
+
[](https://afterpython.org)
|
|
4
|
+
[](https://pixi.sh)
|
|
5
|
+

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

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