afterpython 0.1.0.dev17__py3-none-any.whl → 0.1.1__py3-none-any.whl
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/builders/__init__.py +12 -2
- afterpython/builders/content_json.py +131 -0
- afterpython/builders/index_md.py +94 -0
- afterpython/builders/jupyter_notebook.py +293 -0
- afterpython/builders/markdown.py +271 -0
- afterpython/builders/metadata.py +5 -1
- afterpython/cli/commands/build.py +90 -62
- afterpython/cli/commands/check.py +1 -1
- afterpython/cli/commands/clean.py +2 -2
- afterpython/cli/commands/commit.py +5 -2
- afterpython/cli/commands/commitizen.py +1 -1
- afterpython/cli/commands/dev.py +10 -7
- afterpython/cli/commands/format.py +1 -1
- afterpython/cli/commands/init.py +1 -1
- afterpython/cli/commands/install.py +1 -1
- afterpython/cli/commands/lock.py +1 -1
- afterpython/cli/commands/pre_commit.py +1 -1
- afterpython/cli/commands/preview.py +1 -1
- afterpython/cli/commands/start.py +1 -1
- afterpython/cli/commands/sync.py +33 -6
- afterpython/cli/commands/update.py +5 -5
- afterpython/templates/ci-workflow-template.yml +1 -1
- afterpython/templates/cz-template.toml +0 -1
- afterpython/templates/pre-commit-config-template.yaml +1 -1
- afterpython/tools/_afterpython.py +71 -9
- afterpython/tools/myst.py +42 -3
- afterpython/utils.py +73 -0
- {afterpython-0.1.0.dev17.dist-info → afterpython-0.1.1.dist-info}/METADATA +14 -9
- afterpython-0.1.1.dist-info/RECORD +57 -0
- {afterpython-0.1.0.dev17.dist-info → afterpython-0.1.1.dist-info}/WHEEL +1 -1
- afterpython/builders/molab.py +0 -100
- afterpython-0.1.0.dev17.dist-info/RECORD +0 -54
- {afterpython-0.1.0.dev17.dist-info → afterpython-0.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -7,7 +7,7 @@ from click.exceptions import Exit
|
|
|
7
7
|
@click.command()
|
|
8
8
|
@click.pass_context
|
|
9
9
|
def preview(ctx):
|
|
10
|
-
"""Preview the production build of the project website
|
|
10
|
+
"""Preview the production build of the project website"""
|
|
11
11
|
from afterpython.utils import find_node_env
|
|
12
12
|
|
|
13
13
|
paths = ctx.obj["paths"]
|
|
@@ -42,7 +42,7 @@ command_kwargs = {
|
|
|
42
42
|
"--guide", is_flag=True, help="Start the development server for guide/ directory"
|
|
43
43
|
)
|
|
44
44
|
def start(ctx, doc: bool, blog: bool, tutorial: bool, example: bool, guide: bool):
|
|
45
|
-
"""
|
|
45
|
+
"""Start MyST development server for a specific content type"""
|
|
46
46
|
from afterpython.const import CONTENT_TYPES
|
|
47
47
|
from afterpython.utils import (
|
|
48
48
|
find_node_env,
|
afterpython/cli/commands/sync.py
CHANGED
|
@@ -5,7 +5,7 @@ from pyproject_metadata import StandardMetadata
|
|
|
5
5
|
from tomlkit.toml_document import TOMLDocument
|
|
6
6
|
|
|
7
7
|
import afterpython as ap
|
|
8
|
-
from afterpython.utils import convert_author_name_to_id
|
|
8
|
+
from afterpython.utils import convert_author_name_to_id, normalize_static_path
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _sync_authors_yml(authors: list[tuple[str, str | None]]):
|
|
@@ -36,7 +36,7 @@ def _sync_authors_yml(authors: list[tuple[str, str | None]]):
|
|
|
36
36
|
|
|
37
37
|
@click.command()
|
|
38
38
|
def sync():
|
|
39
|
-
"""Sync between pyproject.toml
|
|
39
|
+
"""Sync configuration between pyproject.toml/afterpython.toml and authors.yml/myst.yml"""
|
|
40
40
|
from afterpython._io.toml import _from_tomlkit
|
|
41
41
|
from afterpython.const import CONTENT_TYPES
|
|
42
42
|
from afterpython.tools._afterpython import read_afterpython
|
|
@@ -54,6 +54,23 @@ def sync():
|
|
|
54
54
|
company_name = str(_from_tomlkit(afterpython.get("company", {})).get("name", ""))
|
|
55
55
|
company_url = str(_from_tomlkit(afterpython.get("company", {})).get("url", ""))
|
|
56
56
|
website_url = str(_from_tomlkit(afterpython.get("website", {})).get("url", ""))
|
|
57
|
+
try:
|
|
58
|
+
website_favicon = normalize_static_path(
|
|
59
|
+
str(_from_tomlkit(afterpython.get("website", {})).get("favicon", ""))
|
|
60
|
+
)
|
|
61
|
+
website_logo = normalize_static_path(
|
|
62
|
+
str(_from_tomlkit(afterpython.get("website", {})).get("logo", ""))
|
|
63
|
+
)
|
|
64
|
+
website_logo_dark = normalize_static_path(
|
|
65
|
+
str(_from_tomlkit(afterpython.get("website", {})).get("logo_dark", ""))
|
|
66
|
+
)
|
|
67
|
+
website_thumbnail = normalize_static_path(
|
|
68
|
+
str(_from_tomlkit(afterpython.get("website", {})).get("thumbnail", ""))
|
|
69
|
+
)
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
click.echo(f"Error in reading afterpython.toml: {e}")
|
|
72
|
+
return
|
|
73
|
+
|
|
57
74
|
authors = pyproject.authors
|
|
58
75
|
if company_name and company_url:
|
|
59
76
|
nav_bar = [{"title": company_name, "url": company_url}]
|
|
@@ -83,9 +100,9 @@ def sync():
|
|
|
83
100
|
# based on the current values in pyproject.toml and afterpython.toml
|
|
84
101
|
for content_type in CONTENT_TYPES:
|
|
85
102
|
path = ap.paths.afterpython_path / content_type
|
|
86
|
-
nav_bar_per_content_type = [
|
|
87
|
-
|
|
88
|
-
]
|
|
103
|
+
# nav_bar_per_content_type = [
|
|
104
|
+
# item for item in nav_bar if item["title"] != content_type.capitalize() + "s"
|
|
105
|
+
# ]
|
|
89
106
|
title = project_name + f"'s {content_type.capitalize()}"
|
|
90
107
|
data = {
|
|
91
108
|
"project": {
|
|
@@ -103,14 +120,24 @@ def sync():
|
|
|
103
120
|
"description": str(pyproject.description),
|
|
104
121
|
"keywords": list(map(str, pyproject.keywords)),
|
|
105
122
|
"github": github_url,
|
|
123
|
+
"thumbnail": "../static" + website_thumbnail
|
|
124
|
+
if website_thumbnail
|
|
125
|
+
else "",
|
|
106
126
|
},
|
|
107
127
|
"site": {
|
|
108
128
|
"title": title,
|
|
109
129
|
"options": {
|
|
130
|
+
"favicon": "../static" + website_favicon if website_favicon else "",
|
|
131
|
+
"logo": "../static" + website_logo if website_logo else "",
|
|
132
|
+
"logo_dark": "../static" + website_logo_dark
|
|
133
|
+
if website_logo_dark
|
|
134
|
+
else "",
|
|
110
135
|
"logo_text": project_name,
|
|
111
136
|
"logo_url": website_url,
|
|
112
137
|
},
|
|
113
|
-
|
|
138
|
+
# FIXME: disable nav bar for now until myst fixes the issue with the nav bar
|
|
139
|
+
# the current issue is they prepend the BASE_URL to the nav bar links even they are external
|
|
140
|
+
# "nav": nav_bar_per_content_type,
|
|
114
141
|
"actions": [
|
|
115
142
|
{
|
|
116
143
|
"title": "⭐ Star",
|
|
@@ -24,15 +24,15 @@ def update():
|
|
|
24
24
|
"-u",
|
|
25
25
|
"--upgrade",
|
|
26
26
|
is_flag=True,
|
|
27
|
-
help="
|
|
27
|
+
help="Upgrade dependencies to their latest versions",
|
|
28
28
|
)
|
|
29
29
|
@click.option(
|
|
30
30
|
"--all",
|
|
31
31
|
is_flag=True,
|
|
32
|
-
help="update
|
|
32
|
+
help="Also update pre-commit hooks and pixi dependencies",
|
|
33
33
|
)
|
|
34
34
|
def dependencies(upgrade: bool, all: bool):
|
|
35
|
-
"""
|
|
35
|
+
"""Check and update project dependencies to latest versions"""
|
|
36
36
|
from afterpython.pcu import get_dependencies, update_dependencies
|
|
37
37
|
from afterpython.utils import has_pixi, has_uv
|
|
38
38
|
|
|
@@ -122,10 +122,10 @@ update.add_command(dependencies, name="deps") # alias for "dependencies"
|
|
|
122
122
|
@click.option(
|
|
123
123
|
"--no-backup",
|
|
124
124
|
is_flag=True,
|
|
125
|
-
help="
|
|
125
|
+
help="Skip creating a backup of the existing website template",
|
|
126
126
|
)
|
|
127
127
|
def website(ctx, no_backup: bool):
|
|
128
|
-
"""Update
|
|
128
|
+
"""Update project website template to the latest version"""
|
|
129
129
|
from afterpython.utils import find_node_env
|
|
130
130
|
|
|
131
131
|
website_template_repo = "AfterPythonOrg/project-website-template"
|
|
@@ -3,14 +3,71 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
6
8
|
from tomlkit.toml_document import TOMLDocument
|
|
7
9
|
|
|
10
|
+
from afterpython._typing import tContentType
|
|
11
|
+
|
|
8
12
|
import tomlkit
|
|
9
13
|
|
|
10
14
|
import afterpython as ap
|
|
11
15
|
from afterpython._io.toml import _to_tomlkit, read_toml, write_toml
|
|
12
16
|
|
|
13
17
|
|
|
18
|
+
def get_default_thumbnail(
|
|
19
|
+
content_type: tContentType, file_path: Path | None = None
|
|
20
|
+
) -> str:
|
|
21
|
+
"""Get the default thumbnail value for a content type.
|
|
22
|
+
|
|
23
|
+
Reads default thumbnail configuration from afterpython.toml:
|
|
24
|
+
- Content type specific thumbnail: [website.{content_type}.thumbnail]
|
|
25
|
+
- Website-wide default thumbnail: [website.thumbnail]
|
|
26
|
+
- Falls back to empty string if no default is configured
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
content_type: The content type (blog, tutorial, etc.)
|
|
30
|
+
file_path: Optional path to the file needing the thumbnail (for calculating relative path)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The thumbnail value to use (e.g., "./static/image.png", "../static/image.png", or "")
|
|
34
|
+
"""
|
|
35
|
+
from afterpython._io.toml import _from_tomlkit
|
|
36
|
+
from afterpython.utils import get_relative_static_prefix, normalize_static_path
|
|
37
|
+
|
|
38
|
+
afterpython = read_afterpython()
|
|
39
|
+
try:
|
|
40
|
+
# website_default_thumbnail = normalize_static_path(
|
|
41
|
+
# str(_from_tomlkit(afterpython.get("website", {})).get("thumbnail", ""))
|
|
42
|
+
# )
|
|
43
|
+
content_type_default_thumbnail = normalize_static_path(
|
|
44
|
+
str(
|
|
45
|
+
_from_tomlkit(afterpython.get("website", {}))
|
|
46
|
+
.get(content_type, {})
|
|
47
|
+
.get("thumbnail", "")
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
except ValueError as e:
|
|
51
|
+
print(f"Error in reading afterpython.toml for {content_type=}: {e}")
|
|
52
|
+
return "null"
|
|
53
|
+
|
|
54
|
+
# Determine the static prefix based on file location
|
|
55
|
+
if file_path is not None:
|
|
56
|
+
content_type_path = ap.paths.afterpython_path / content_type.lower()
|
|
57
|
+
static_prefix = get_relative_static_prefix(file_path, content_type_path)
|
|
58
|
+
else:
|
|
59
|
+
static_prefix = "./" # Default fallback
|
|
60
|
+
|
|
61
|
+
if content_type_default_thumbnail:
|
|
62
|
+
return f"{static_prefix}static{content_type_default_thumbnail}"
|
|
63
|
+
# use "thumbnail" set in afterpython.toml [website]
|
|
64
|
+
# elif website_default_thumbnail:
|
|
65
|
+
# Website-wide static is one level up from content_type folder
|
|
66
|
+
# return f"{static_prefix}../static{website_default_thumbnail}"
|
|
67
|
+
else:
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
|
|
14
71
|
def read_afterpython() -> TOMLDocument:
|
|
15
72
|
"""Read afterpython.toml"""
|
|
16
73
|
return read_toml(ap.paths.afterpython_path / "afterpython.toml")
|
|
@@ -55,12 +112,17 @@ def init_afterpython():
|
|
|
55
112
|
return
|
|
56
113
|
afterpython_toml_path.touch()
|
|
57
114
|
print(f"Created {afterpython_toml_path}")
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
115
|
+
default_data = {
|
|
116
|
+
"company": {
|
|
117
|
+
"name": "",
|
|
118
|
+
"url": "",
|
|
119
|
+
},
|
|
120
|
+
"website": {
|
|
121
|
+
"url": "",
|
|
122
|
+
"favicon": "favicon.svg",
|
|
123
|
+
"logo": "logo.svg",
|
|
124
|
+
"logo_dark": "logo.svg",
|
|
125
|
+
"thumbnail": "thumbnail.png",
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
update_afterpython(default_data)
|
afterpython/tools/myst.py
CHANGED
|
@@ -103,6 +103,48 @@ def update_myst_yml(data_update: dict, path: Path, add_comments: bool = False):
|
|
|
103
103
|
write_yaml(file_path, existing_data)
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def _write_index_file(content_type: tContentType):
|
|
107
|
+
"""Create a placeholder index.md file for MyST to prevent first file from becoming index.
|
|
108
|
+
|
|
109
|
+
MyST treats the first file in TOC as index. This placeholder ensures all actual
|
|
110
|
+
content files get proper slugs. The generated index.html is deleted post-build
|
|
111
|
+
since SvelteKit owns the landing page route.
|
|
112
|
+
|
|
113
|
+
Note: This only creates the file. TOC modification is handled by the build process.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
FileExistsError: If index.md already exists (users shouldn't create this file).
|
|
117
|
+
"""
|
|
118
|
+
if content_type == "doc":
|
|
119
|
+
return # Doc doesn't need a placeholder index.md
|
|
120
|
+
|
|
121
|
+
content_path = ap.paths.afterpython_path / content_type
|
|
122
|
+
index_file = content_path / "index.md"
|
|
123
|
+
|
|
124
|
+
# Check if user created an index.md file
|
|
125
|
+
if index_file.exists():
|
|
126
|
+
raise FileExistsError(
|
|
127
|
+
f"\n"
|
|
128
|
+
f"Found existing 'index.md' in afterpython/{content_type}/\n"
|
|
129
|
+
f"\n"
|
|
130
|
+
f"The 'index.md' file is reserved for internal use by AfterPython.\n"
|
|
131
|
+
f"MyST treats the first file in TOC as index, which would conflict with\n"
|
|
132
|
+
f"SvelteKit (project-website-template)'s landing page route (/{content_type}).\n"
|
|
133
|
+
f"\n"
|
|
134
|
+
f"Please rename your file to something else (e.g., '{content_type}_intro.md')\n"
|
|
135
|
+
f"and update the reference in afterpython/{content_type}/myst.yml"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
index_content = f"""---
|
|
139
|
+
title: ← {content_type.capitalize()}
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
This is a placeholder index page. The actual {content_type} landing page is rendered by SvelteKit.
|
|
143
|
+
"""
|
|
144
|
+
index_file.write_text(index_content)
|
|
145
|
+
return index_file
|
|
146
|
+
|
|
147
|
+
|
|
106
148
|
def _write_welcome_file(content_type: tContentType):
|
|
107
149
|
welcome_file = ap.paths.afterpython_path / content_type / "index.md"
|
|
108
150
|
if welcome_file.exists():
|
|
@@ -156,9 +198,6 @@ def init_myst():
|
|
|
156
198
|
},
|
|
157
199
|
"site": {
|
|
158
200
|
"options": {
|
|
159
|
-
"favicon": "../static/favicon.svg",
|
|
160
|
-
"logo": "../static/logo.svg",
|
|
161
|
-
"logo_dark": "../static/logo.svg",
|
|
162
201
|
"analytics_google": "{{ GOOGLE_ANALYTICS_ID }}",
|
|
163
202
|
# "twitter": "",
|
|
164
203
|
},
|
afterpython/utils.py
CHANGED
|
@@ -224,3 +224,76 @@ def handle_passthrough_help(
|
|
|
224
224
|
subprocess.run([*underlying_command, "--help"])
|
|
225
225
|
|
|
226
226
|
ctx.exit(0)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def normalize_static_path(path: str) -> str:
|
|
230
|
+
"""
|
|
231
|
+
Normalize paths for static assets.
|
|
232
|
+
|
|
233
|
+
Only allows two formats:
|
|
234
|
+
1. Pure filename/path without ./ or ../ (e.g., "logo.svg", "somewhere/logo.svg")
|
|
235
|
+
2. Absolute path starting with / (e.g., "/somewhere/logo.svg")
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
path: User-provided path from afterpython.toml
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Normalized path
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValueError: If path contains ./ or ../ or other invalid patterns
|
|
245
|
+
|
|
246
|
+
Examples:
|
|
247
|
+
>>> normalize_static_path("logo.svg")
|
|
248
|
+
'logo.svg'
|
|
249
|
+
>>> normalize_static_path("somewhere/logo.svg")
|
|
250
|
+
'/somewhere/logo.svg'
|
|
251
|
+
>>> normalize_static_path("/somewhere/logo.svg")
|
|
252
|
+
'/somewhere/logo.svg'
|
|
253
|
+
>>> normalize_static_path("./logo.svg") # Raises ValueError
|
|
254
|
+
>>> normalize_static_path("../logo.svg") # Raises ValueError
|
|
255
|
+
>>> normalize_static_path("static/logo.svg") # Raises ValueError
|
|
256
|
+
"""
|
|
257
|
+
if not path:
|
|
258
|
+
return ""
|
|
259
|
+
|
|
260
|
+
# Reject paths with ./ or ../
|
|
261
|
+
if "./" in path or "../" in path or path.startswith(".") or path.startswith(".."):
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"Invalid path '{path}': paths cannot contain './' or '../'. "
|
|
264
|
+
f"Use either a pure filename (e.g., 'logo.svg') or absolute path (e.g., '/somewhere/logo.svg')"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Reject paths with 'static' in them
|
|
268
|
+
if "static" in path.lower():
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Invalid path '{path}': do not include 'static' in the path. "
|
|
271
|
+
f"Files are assumed to be in the static/ directory."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# If it starts with /, it's absolute - keep as is
|
|
275
|
+
if not path.startswith("/"):
|
|
276
|
+
return "/" + path
|
|
277
|
+
else:
|
|
278
|
+
return path
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_relative_static_prefix(file_path: Path, content_type_path: Path) -> str:
|
|
282
|
+
"""Calculate the relative path prefix from a file to its content type's static/ folder.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
file_path: Path to the content file (e.g., blog/layer/blog5.ipynb)
|
|
286
|
+
content_type_path: Path to the content type folder (e.g., blog/)
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Relative prefix to reach static/ (e.g., "./" or "../" or "../../")
|
|
290
|
+
"""
|
|
291
|
+
# Get the depth: how many directories deep the file is from content_type_path
|
|
292
|
+
relative = file_path.parent.relative_to(content_type_path)
|
|
293
|
+
depth = len(relative.parts)
|
|
294
|
+
|
|
295
|
+
# Build the correct prefix
|
|
296
|
+
if depth == 0:
|
|
297
|
+
return "./" # File is directly in content_type_path
|
|
298
|
+
else:
|
|
299
|
+
return "../" * depth
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: afterpython
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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
|
|
@@ -20,12 +20,11 @@ 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
|
|
24
23
|
Requires-Dist: python-dotenv>=1.2.1
|
|
25
24
|
Requires-Dist: pyproject-metadata>=0.10.0
|
|
26
|
-
Requires-Dist: mystmd>=1.
|
|
25
|
+
Requires-Dist: mystmd>=1.7.0
|
|
27
26
|
Requires-Dist: pdoc>=16.0.0
|
|
28
|
-
Requires-Dist: ruff>=0.14.
|
|
27
|
+
Requires-Dist: ruff>=0.14.8
|
|
29
28
|
Requires-Dist: pre-commit>=4.5.0
|
|
30
29
|
Requires-Dist: commitizen>=4.10.0
|
|
31
30
|
Requires-Python: >=3.11
|
|
@@ -105,6 +104,17 @@ ap init
|
|
|
105
104
|
```
|
|
106
105
|
|
|
107
106
|
|
|
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
|
+
|
|
108
118
|
---
|
|
109
119
|
## Tech Stack
|
|
110
120
|
- [MyST]
|
|
@@ -119,8 +129,3 @@ ap init
|
|
|
119
129
|
- [WebLLM]
|
|
120
130
|
<!-- - ty -->
|
|
121
131
|
<!-- - [pixi] -->
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
## *Our Mission*
|
|
126
|
-
> Only by making package maintenance and documentation easy can we encourage more Python developers to publish their own packages and grow the ecosystem even further.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
afterpython/__init__.py,sha256=aNochs72gU2NR1cBzIkk3NxSsdJ7ceHluI5gdYwaFpI,289
|
|
2
|
+
afterpython/_io/toml.py,sha256=dvmq6n4MhlJ02u_YjfC3mOnCWatfCn_J1lugavigoJw,3410
|
|
3
|
+
afterpython/_io/yaml.py,sha256=raksve_gaIFJim8lQrq-kQHkRjTloed7CT2FY0oUJSg,654
|
|
4
|
+
afterpython/_paths.py,sha256=FTwQ7necKYuRosCyMZPfv1eqaEmGIsyqd3yXpm-WQTA,1584
|
|
5
|
+
afterpython/_typing.py,sha256=FbphSGoujPuTfCZqt0HaKQrBgzhRpCOEAEQU6Cbl7oA,145
|
|
6
|
+
afterpython/builders/__init__.py,sha256=YMfYfsbSJNMjqfq9GRsQj8ayWUhtZKpvrc0oFUASICo,575
|
|
7
|
+
afterpython/builders/content_json.py,sha256=-LTkeA8oA6XEudc2b1H7U0-UwcJlHn7xgICeweMYrG0,4805
|
|
8
|
+
afterpython/builders/index_md.py,sha256=SDVHnVftL4D1AA8jw2YvRdTem_Ho8v8DcrORVjVtW8A,3762
|
|
9
|
+
afterpython/builders/jupyter_notebook.py,sha256=2H5Qv_IIkmOud-GYZPYjB0E30fVZubpUUQ5gFrtGSqU,10140
|
|
10
|
+
afterpython/builders/markdown.py,sha256=0ExfRqvOTWsgUkAVaGGNkiNDtNedRr7JR1yTGxpbsmA,8736
|
|
11
|
+
afterpython/builders/metadata.py,sha256=--0zWaI0ZI1wKeezcyqSUy-h1IuLqnI33ateoAHIaBU,1337
|
|
12
|
+
afterpython/cli/__init__.py,sha256=G4AB-rllhmr7tURFc5cXJfkjZZ2bXtMBg3oy4zUGEFw,84
|
|
13
|
+
afterpython/cli/commands/add.py,sha256=Ppp4msdvOwN9_-fdG5-b0-IV9p0Eef9CiIEU3srUjYk,1792
|
|
14
|
+
afterpython/cli/commands/build.py,sha256=Vvyd7AkL2Tw3vEDhi4bEOWD9hdtxon7SgAzXUeh5D8w,9050
|
|
15
|
+
afterpython/cli/commands/bump.py,sha256=2MZBQ1kxjr0bWhA--_xHwdwjsUlCyrXPn3pJ5NDZO4k,4088
|
|
16
|
+
afterpython/cli/commands/check.py,sha256=lrPaVThAwxxnczu-VOZT7dd7aKjstnMQfX9UpZY3Lxs,1058
|
|
17
|
+
afterpython/cli/commands/clean.py,sha256=havP43ZC1AKFzvscsRMF_to6TvOwruxGA5qzr6dMHvg,2529
|
|
18
|
+
afterpython/cli/commands/commit.py,sha256=9QOKyt4tfTJzZEdHEPAdpUc4stYMSvG3-31uADuBxn4,1420
|
|
19
|
+
afterpython/cli/commands/commitizen.py,sha256=H3jewz4VkZi5FYTWw3USVM3XosRx12kPShOQGxD3LNM,842
|
|
20
|
+
afterpython/cli/commands/dev.py,sha256=YIJECfroGRQnuMWWRC4W_DMA0eJoQssufj8WBDjQVOM,6796
|
|
21
|
+
afterpython/cli/commands/format.py,sha256=VFwCG9KDTF6j4Y_Ewp4_RpS8uNBQNOAj9HU_G97my5A,710
|
|
22
|
+
afterpython/cli/commands/init.py,sha256=UiE8QIhDV9NqoqU2EU_FMAlJ2Y18lFF0x2JmcyiAQxc,3044
|
|
23
|
+
afterpython/cli/commands/init_branch_rules.py,sha256=fzAm8D5qbW3u4bEMpqPuRgO-0akuCGG7pu09JlKhYn4,243
|
|
24
|
+
afterpython/cli/commands/install.py,sha256=B8JuQmtU_4EZpHct2J5j30BGLkGmvwKvU26upbmabvQ,750
|
|
25
|
+
afterpython/cli/commands/lock.py,sha256=Kppu_0tnqdbP6Gbulxf123f-cFzTGIw39si2O7rDo3Q,603
|
|
26
|
+
afterpython/cli/commands/pre_commit.py,sha256=QIuzTZEfw4uPMqmQT8YmyD0yewBcwjLvcnbVjRbonQE,1978
|
|
27
|
+
afterpython/cli/commands/preview.py,sha256=sBwe46H0V-U7I4d0-u-nXmPBWGmkjKhkUTbqV927zRM,593
|
|
28
|
+
afterpython/cli/commands/release.py,sha256=6SDEnPtuCL0_XUqWUlvRSKxAv011QqOD0kc5pKu7378,2479
|
|
29
|
+
afterpython/cli/commands/remove.py,sha256=Fl79JSI9IdHyzYflocXxcxgBkBPkNFAW98Yuijly26g,1812
|
|
30
|
+
afterpython/cli/commands/start.py,sha256=q4rUkIllOO8wJQ9ABtsx-dqkW1yC0vltAtSkiHN_GH4,4002
|
|
31
|
+
afterpython/cli/commands/sync.py,sha256=u9L3aq04M5VHh0FByDSpmBD7F1RYCNsIgvZFifnRZ6A,6042
|
|
32
|
+
afterpython/cli/commands/update.py,sha256=4h_zKp72e4RmXUKHyeP6kfHqr4V3IugSOnxa-E1F55I,6312
|
|
33
|
+
afterpython/cli/main.py,sha256=m3BJ5rOlllUyOngD2HF2fosDLrduREzAQKpsTqcYPZc,4956
|
|
34
|
+
afterpython/const.py,sha256=B_yUBmHPyu98aOibwt7idWbhKNBBed0TeYXNBd8tPTk,157
|
|
35
|
+
afterpython/main.py,sha256=w0z6mTib3u4p2HxMrk-tIJlZtur7uRaqsbE0ewlYlSs,490
|
|
36
|
+
afterpython/pcu.py,sha256=EoEhSzFZ49b5qc9KH2RLxIHSzY8YFM5uhtw07chcrr8,6093
|
|
37
|
+
afterpython/templates/ci-workflow-template.yml,sha256=w0BiV9F_y9YNTd6mTD7xRlJgwRQw61AinhiACXlEQqk,5933
|
|
38
|
+
afterpython/templates/cz-template.toml,sha256=2qwbQLb0nbZaA63oWXFFzvzPNBsFZHATL1C2rdNdjjs,185
|
|
39
|
+
afterpython/templates/dependabot-template.yml,sha256=CBQEgh8EsXw2X7IqzGtFI7lVT9liF6PW4DvGLB0X2LY,1136
|
|
40
|
+
afterpython/templates/deploy-workflow-template.yml,sha256=2W4K3lKMYjSTPrlXIsMweBzqP9IoLtZXAAcUu5FA5NA,2365
|
|
41
|
+
afterpython/templates/pre-commit-config-template.yaml,sha256=u_JF4YHGHIWxLdcX5BS__VsVgOwwVQQsSZGPNR2Yyf0,915
|
|
42
|
+
afterpython/templates/release-workflow-template.yml,sha256=3YBkgrqmi--uznslpMxPz7Vj7L4_NPlhDxgwYBt-Ui8,1606
|
|
43
|
+
afterpython/templates/ruff-template.toml,sha256=lNCd4EthoLScr4cyVEgGUH4xK5CfIX3f9I3zkJHAYgg,1008
|
|
44
|
+
afterpython/tools/_afterpython.py,sha256=G1075u8VT6s-TjEcGajqEYS3pZoW0sGyJM7DJohMOPE,4244
|
|
45
|
+
afterpython/tools/_git.py,sha256=aO-qDEQ4OevD_B74DYKK4nQKYeiaS1BXmYc8paJRfbQ,3772
|
|
46
|
+
afterpython/tools/branch_rules.py,sha256=ciQ2H2YBnoCZu_plyVeIHzf2FcAILRFhpjzOR2_IfJg,3159
|
|
47
|
+
afterpython/tools/commitizen.py,sha256=ZO1qHmzUU0myd85tncv76WtIR7DB_AoQGSUvaf78Ohg,438
|
|
48
|
+
afterpython/tools/github_actions.py,sha256=zVmg9aqUg-EO0CnbRHl9i8TmFtXpt7nd0VjzckqqE30,1527
|
|
49
|
+
afterpython/tools/myst.py,sha256=2VO-gD25PbGSD8-v1sLI1v3Q9loahRd3ednKrW4_Utc,8113
|
|
50
|
+
afterpython/tools/pre_commit.py,sha256=oNsrUkPI13rSSBSyhyTTKPnQGhDxLIPYhxu6kL0fDmo,1206
|
|
51
|
+
afterpython/tools/pyproject.py,sha256=kb7BDn24yH_xS2F5_L0dK-GL4KgOJS-MIIUQhHnLorI,3507
|
|
52
|
+
afterpython/utils.py,sha256=s1FIJP6FvpAcK8SujB8glFbiKx-ZCltywh5ihaOhL0U,9516
|
|
53
|
+
afterpython/version.py,sha256=hihRV1S-gc1m46OaXD32ikkEkqzs-Gtq7AcAqOQV-wY,1603
|
|
54
|
+
afterpython-0.1.1.dist-info/WHEEL,sha256=93kfTGt3a0Dykt_T-gsjtyS5_p8F_d6CE1NwmBOirzo,79
|
|
55
|
+
afterpython-0.1.1.dist-info/entry_points.txt,sha256=roa0PDgBueM1JPbRG0G2OSxzaN394OUL_oS9iEwVLBc,80
|
|
56
|
+
afterpython-0.1.1.dist-info/METADATA,sha256=aUAV_sBsqyVQouDgnzOfN7X96zSu4p7R_GB6FUvJr_o,5779
|
|
57
|
+
afterpython-0.1.1.dist-info/RECORD,,
|
afterpython/builders/molab.py
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
if TYPE_CHECKING:
|
|
6
|
-
from afterpython._typing import tContentType
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from pyproject_metadata import StandardMetadata
|
|
12
|
-
|
|
13
|
-
import afterpython as ap
|
|
14
|
-
from afterpython.const import CONTENT_TYPES
|
|
15
|
-
from afterpython.tools.pyproject import read_metadata
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _get_molab_badge() -> str:
|
|
19
|
-
return "https://marimo.io/molab-shield.svg"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _create_molab_url(github_url: str, content_path: Path):
|
|
23
|
-
"""Create a molab URL for a given content type and notebook path.
|
|
24
|
-
Args:
|
|
25
|
-
github_url: str, e.g. "https://github.com/AfterPythonOrg/afterpython"
|
|
26
|
-
content_path: str, e.g. "tutorial/test.ipynb"
|
|
27
|
-
"""
|
|
28
|
-
github_url = github_url.replace("https://github.com/", "github/")
|
|
29
|
-
return f"https://molab.marimo.io/{github_url}/blob/main/afterpython/{content_path.as_posix()}"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def add_molab_badge_to_jupyter_notebooks(content_type: tContentType):
|
|
33
|
-
"""Add a badge markdown cell to the top of Jupyter notebooks in the given content type directory, e.g. tutorial/, blog/, etc.
|
|
34
|
-
# NOTE
|
|
35
|
-
Note that the jupyter notebooks need to exist in the github repository first.
|
|
36
|
-
If you have renamed a notebook, you need to push the changes to the github repository first for the badge to work.
|
|
37
|
-
"""
|
|
38
|
-
assert content_type.lower() in CONTENT_TYPES, (
|
|
39
|
-
f"Invalid content type: {content_type}"
|
|
40
|
-
)
|
|
41
|
-
metadata: StandardMetadata = read_metadata()
|
|
42
|
-
|
|
43
|
-
if "repository" not in metadata.urls:
|
|
44
|
-
return "Repository URL not found in [project.urls] in pyproject.toml, cannot add molab badge"
|
|
45
|
-
else:
|
|
46
|
-
github_url = metadata.urls["repository"]
|
|
47
|
-
|
|
48
|
-
path = ap.paths.afterpython_path / content_type.lower()
|
|
49
|
-
|
|
50
|
-
# iterate over all files in the path
|
|
51
|
-
for notebook_path in path.rglob("*.ipynb"):
|
|
52
|
-
# Skip files in _build directory
|
|
53
|
-
if "_build" in notebook_path.parts:
|
|
54
|
-
continue
|
|
55
|
-
|
|
56
|
-
# Get path relative to afterpython/ (includes tutorial/ in the path)
|
|
57
|
-
content_path = notebook_path.relative_to(ap.paths.afterpython_path)
|
|
58
|
-
molab_url = _create_molab_url(github_url, content_path)
|
|
59
|
-
badge_md = f"[})]({molab_url})"
|
|
60
|
-
|
|
61
|
-
# Read the notebook
|
|
62
|
-
try:
|
|
63
|
-
# Read the notebook
|
|
64
|
-
with open(notebook_path) as f:
|
|
65
|
-
notebook = json.load(f)
|
|
66
|
-
except json.JSONDecodeError:
|
|
67
|
-
print(f"✗ Skipping {content_path} - invalid or empty JSON")
|
|
68
|
-
continue
|
|
69
|
-
except Exception as e:
|
|
70
|
-
print(f"✗ Error reading {content_path}: {e}")
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
# Validate notebook structure
|
|
74
|
-
if "cells" not in notebook:
|
|
75
|
-
print(f"✗ Skipping {content_path} - not a valid Jupyter notebook")
|
|
76
|
-
continue
|
|
77
|
-
|
|
78
|
-
# Create a markdown cell with the badge
|
|
79
|
-
badge_cell = {
|
|
80
|
-
"cell_type": "markdown",
|
|
81
|
-
"id": "molab-badge-cell", # Unique ID for the badge cell
|
|
82
|
-
"metadata": {
|
|
83
|
-
"tags": ["molab", "hide-input"] # Optional: use MyST tags
|
|
84
|
-
},
|
|
85
|
-
"source": [badge_md],
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
# Check if first cell is already a badge cell (avoid duplicates)
|
|
89
|
-
if notebook["cells"] and notebook["cells"][0].get("id") == "molab-badge-cell":
|
|
90
|
-
# Update existing badge
|
|
91
|
-
notebook["cells"][0] = badge_cell
|
|
92
|
-
print(f"✓ Updated molab badge in: {content_path}")
|
|
93
|
-
else:
|
|
94
|
-
# Insert at the beginning
|
|
95
|
-
notebook["cells"].insert(0, badge_cell)
|
|
96
|
-
print(f"✓ Added molab badge to: {content_path}")
|
|
97
|
-
|
|
98
|
-
# Write back
|
|
99
|
-
with open(notebook_path, "w") as f:
|
|
100
|
-
json.dump(notebook, f, indent=1)
|