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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
import afterpython as ap
|
|
6
|
+
from afterpython.const import CONTENT_TYPES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _read_markdown(markdown_path: Path, content_path: Path) -> str | None:
|
|
10
|
+
"""Read and validate a markdown file.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
markdown_path: Absolute path to the markdown file
|
|
14
|
+
content_path: Path relative to afterpython/ for logging
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Markdown content as a string if valid, None if invalid or error occurred
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
with open(markdown_path, encoding="utf-8") as f:
|
|
21
|
+
content = f.read()
|
|
22
|
+
return content
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
click.echo(f"✗ Skipping {content_path} - file not found")
|
|
25
|
+
return None
|
|
26
|
+
except Exception as e:
|
|
27
|
+
click.echo(f"✗ Error reading {content_path}: {e}")
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _write_markdown(content: str, markdown_path: Path):
|
|
32
|
+
"""Write markdown content to file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
content: The markdown content to write
|
|
36
|
+
markdown_path: Absolute path to the markdown file
|
|
37
|
+
"""
|
|
38
|
+
with open(markdown_path, "w", encoding="utf-8") as f:
|
|
39
|
+
f.write(content)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _has_frontmatter(content: str) -> bool:
|
|
43
|
+
"""Check if markdown content has frontmatter.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
content: The markdown file content as a string
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if the content starts with ---, False otherwise
|
|
50
|
+
"""
|
|
51
|
+
return content.strip().startswith("---")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_frontmatter(content: str) -> str | None:
|
|
55
|
+
"""Read frontmatter content from markdown.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
content: The markdown file content as a string
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Frontmatter content as a string (including --- delimiters) if it exists, None otherwise
|
|
62
|
+
"""
|
|
63
|
+
if not _has_frontmatter(content):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
lines = content.split("\n")
|
|
67
|
+
|
|
68
|
+
# Find the closing --- of frontmatter
|
|
69
|
+
closing_index = -1
|
|
70
|
+
for i in range(1, len(lines)):
|
|
71
|
+
if lines[i].strip() == "---":
|
|
72
|
+
closing_index = i
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if closing_index == -1:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Return frontmatter including both --- delimiters
|
|
79
|
+
return "\n".join(lines[: closing_index + 1])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _write_frontmatter(content: str, frontmatter: str) -> str:
|
|
83
|
+
"""Write frontmatter content to markdown.
|
|
84
|
+
|
|
85
|
+
Low-level function that handles frontmatter replacement.
|
|
86
|
+
If frontmatter exists in content, replaces it. If not, prepends it.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
content: The markdown file content as a string
|
|
90
|
+
frontmatter: The complete frontmatter content as a string (should include --- delimiters)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The updated markdown content with the new frontmatter
|
|
94
|
+
"""
|
|
95
|
+
if _has_frontmatter(content):
|
|
96
|
+
# Replace existing frontmatter
|
|
97
|
+
lines = content.split("\n")
|
|
98
|
+
|
|
99
|
+
# Find the closing --- of existing frontmatter
|
|
100
|
+
closing_index = -1
|
|
101
|
+
for i in range(1, len(lines)):
|
|
102
|
+
if lines[i].strip() == "---":
|
|
103
|
+
closing_index = i
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if closing_index > -1:
|
|
107
|
+
# Remove old frontmatter and prepend new one
|
|
108
|
+
body = "\n".join(lines[closing_index + 1 :])
|
|
109
|
+
return frontmatter + "\n" + body
|
|
110
|
+
else:
|
|
111
|
+
# Malformed frontmatter, just prepend new frontmatter
|
|
112
|
+
return frontmatter + "\n" + content
|
|
113
|
+
else:
|
|
114
|
+
# No frontmatter exists, prepend it
|
|
115
|
+
return frontmatter + "\n" + content
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _ensure_blank_line_after_frontmatter(content: str) -> str:
|
|
119
|
+
"""Ensure there's a blank line between frontmatter and content.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
content: The markdown content
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Content with blank line after frontmatter if frontmatter exists
|
|
126
|
+
"""
|
|
127
|
+
if not _has_frontmatter(content):
|
|
128
|
+
return content
|
|
129
|
+
|
|
130
|
+
lines = content.split("\n")
|
|
131
|
+
|
|
132
|
+
# Find the closing --- of frontmatter
|
|
133
|
+
closing_index = -1
|
|
134
|
+
for i in range(1, len(lines)):
|
|
135
|
+
if lines[i].strip() == "---":
|
|
136
|
+
closing_index = i
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
if closing_index == -1:
|
|
140
|
+
return content
|
|
141
|
+
|
|
142
|
+
# Check if there's already a blank line after the closing ---, and if the next line is not empty, insert a blank line
|
|
143
|
+
if closing_index + 1 < len(lines) and lines[closing_index + 1].strip() != "":
|
|
144
|
+
lines.insert(closing_index + 1, "")
|
|
145
|
+
|
|
146
|
+
return "\n".join(lines)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _add_fields_to_frontmatter(content: str, fields: dict[str, str]) -> str:
|
|
150
|
+
"""Add or update fields in frontmatter.
|
|
151
|
+
|
|
152
|
+
High-level function that handles frontmatter field manipulation.
|
|
153
|
+
If frontmatter exists, adds new fields to it. If not, creates new frontmatter with those fields.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
content: The markdown file content as a string
|
|
157
|
+
fields: A dict of fields to add/update (e.g., {"thumbnail": "null"})
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The updated markdown content with the new fields in frontmatter
|
|
161
|
+
"""
|
|
162
|
+
existing_frontmatter = _read_frontmatter(content)
|
|
163
|
+
|
|
164
|
+
if existing_frontmatter:
|
|
165
|
+
# Parse existing frontmatter and add new fields
|
|
166
|
+
lines = existing_frontmatter.split("\n")
|
|
167
|
+
|
|
168
|
+
# Find the closing --- of frontmatter
|
|
169
|
+
closing_index = -1
|
|
170
|
+
for i in range(1, len(lines)):
|
|
171
|
+
if lines[i].strip() == "---":
|
|
172
|
+
closing_index = i
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
if closing_index > 0:
|
|
176
|
+
# Insert new fields before the closing ---
|
|
177
|
+
for key, value in fields.items():
|
|
178
|
+
# Check if field already exists
|
|
179
|
+
if not any(line.strip().startswith(f"{key}:") for line in lines):
|
|
180
|
+
lines.insert(closing_index, f"{key}: {value}")
|
|
181
|
+
closing_index += 1 # Adjust index for next insertion
|
|
182
|
+
|
|
183
|
+
new_frontmatter = "\n".join(lines)
|
|
184
|
+
return _write_frontmatter(content, new_frontmatter)
|
|
185
|
+
else:
|
|
186
|
+
# Create new frontmatter with the provided fields
|
|
187
|
+
field_lines = [f"{key}: {value}" for key, value in fields.items()]
|
|
188
|
+
new_frontmatter = "---\n" + "\n".join(field_lines) + "\n---"
|
|
189
|
+
return _write_frontmatter(content, new_frontmatter)
|
|
190
|
+
|
|
191
|
+
return content
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _add_thumbnail_to_markdown(
|
|
195
|
+
content: str, thumbnail: str, content_path: Path
|
|
196
|
+
) -> tuple[str, bool]:
|
|
197
|
+
"""Add thumbnail field to a markdown file's frontmatter.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
content: The markdown content
|
|
201
|
+
thumbnail: The thumbnail value to add
|
|
202
|
+
content_path: Path for logging purposes
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (modified_content, was_modified)
|
|
206
|
+
"""
|
|
207
|
+
# Check if thumbnail is already set in frontmatter
|
|
208
|
+
frontmatter_content = _read_frontmatter(content)
|
|
209
|
+
if frontmatter_content and "thumbnail:" in frontmatter_content:
|
|
210
|
+
return content, False
|
|
211
|
+
|
|
212
|
+
# Add thumbnail using the fields parameter
|
|
213
|
+
modified_content = _add_fields_to_frontmatter(
|
|
214
|
+
content, fields={"thumbnail": thumbnail}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if frontmatter_content:
|
|
218
|
+
click.echo(
|
|
219
|
+
f" ↳ Added 'thumbnail: {thumbnail}' to frontmatter in: {content_path}"
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
click.echo(
|
|
223
|
+
f" ↳ Created frontmatter with 'thumbnail: {thumbnail}' in: {content_path}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return modified_content, True
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def build_markdown():
|
|
230
|
+
"""Build markdown files for all content types"""
|
|
231
|
+
from afterpython.tools._afterpython import get_default_thumbnail
|
|
232
|
+
|
|
233
|
+
for content_type in CONTENT_TYPES:
|
|
234
|
+
# Skip doc content type - docs don't need default thumbnails
|
|
235
|
+
if content_type == "doc":
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
click.echo(f"Building markdown files for {content_type}/...")
|
|
239
|
+
|
|
240
|
+
path = ap.paths.afterpython_path / content_type.lower()
|
|
241
|
+
|
|
242
|
+
# Iterate over all .md files
|
|
243
|
+
for markdown_path in path.rglob("*.md"):
|
|
244
|
+
# Skip files in _build directory
|
|
245
|
+
if "_build" in markdown_path.parts:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# Get path relative to afterpython/
|
|
249
|
+
content_path = markdown_path.relative_to(ap.paths.afterpython_path)
|
|
250
|
+
|
|
251
|
+
# Get default thumbnail for this content type
|
|
252
|
+
default_thumbnail = get_default_thumbnail(content_type, markdown_path)
|
|
253
|
+
|
|
254
|
+
# Read the markdown file
|
|
255
|
+
content = _read_markdown(markdown_path, content_path)
|
|
256
|
+
if content is None:
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# Ensure blank line after frontmatter (fixes existing files)
|
|
260
|
+
original_content = content
|
|
261
|
+
content = _ensure_blank_line_after_frontmatter(content)
|
|
262
|
+
blank_line_added = content != original_content
|
|
263
|
+
|
|
264
|
+
# Add thumbnail to frontmatter
|
|
265
|
+
modified_content, was_modified = _add_thumbnail_to_markdown(
|
|
266
|
+
content, default_thumbnail, content_path
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Write back if modified (either blank line or thumbnail)
|
|
270
|
+
if was_modified or blank_line_added:
|
|
271
|
+
_write_markdown(modified_content, markdown_path)
|
afterpython/builders/metadata.py
CHANGED
|
@@ -7,6 +7,8 @@ if TYPE_CHECKING:
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
|
|
10
|
+
import click
|
|
11
|
+
|
|
10
12
|
import afterpython as ap
|
|
11
13
|
|
|
12
14
|
build_path = ap.paths.build_path
|
|
@@ -30,13 +32,15 @@ def convert_paths():
|
|
|
30
32
|
with open(build_path / "metadata.json", "w") as f:
|
|
31
33
|
json.dump(metadata, f, indent=2)
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
click.echo("Completed path conversion in metadata.json")
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def build_metadata():
|
|
37
39
|
"""Build metadata.json using pyproject.toml"""
|
|
38
40
|
from afterpython.tools.pyproject import read_metadata
|
|
39
41
|
|
|
42
|
+
click.echo("Building metadata.json...")
|
|
43
|
+
|
|
40
44
|
metadata: StandardMetadata = read_metadata()
|
|
41
45
|
|
|
42
46
|
# Write to metadata.json
|
|
@@ -8,7 +8,6 @@ if TYPE_CHECKING:
|
|
|
8
8
|
|
|
9
9
|
from afterpython._typing import NodeEnv
|
|
10
10
|
|
|
11
|
-
import os
|
|
12
11
|
import shutil
|
|
13
12
|
import subprocess
|
|
14
13
|
|
|
@@ -17,11 +16,14 @@ from click.exceptions import Exit
|
|
|
17
16
|
|
|
18
17
|
import afterpython as ap
|
|
19
18
|
from afterpython.builders import (
|
|
20
|
-
|
|
19
|
+
build_content_json,
|
|
20
|
+
build_jupyter_notebooks,
|
|
21
|
+
build_markdown,
|
|
21
22
|
build_metadata,
|
|
23
|
+
create_placeholder_index_md_files,
|
|
24
|
+
delete_placeholder_index_md_files,
|
|
22
25
|
)
|
|
23
26
|
from afterpython.const import CONTENT_TYPES
|
|
24
|
-
from afterpython.utils import find_node_env
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def determine_base_path() -> str:
|
|
@@ -69,22 +71,31 @@ def prebuild():
|
|
|
69
71
|
"Run 'ap init' first to set up your project."
|
|
70
72
|
)
|
|
71
73
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
+
def _clean_up_builds():
|
|
75
|
+
click.echo("Cleaning up builds...")
|
|
74
76
|
build_path = ap.paths.build_path
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
website_build_path = ap.paths.website_path / "build"
|
|
78
|
+
# clean up content builds in _website/static/{content_type}
|
|
79
|
+
website_static_path = ap.paths.website_path / "static"
|
|
80
|
+
content_build_paths = [
|
|
81
|
+
website_static_path / content_type for content_type in CONTENT_TYPES
|
|
82
|
+
]
|
|
83
|
+
for path in [build_path, website_build_path, *content_build_paths]:
|
|
84
|
+
if path.exists():
|
|
85
|
+
shutil.rmtree(path)
|
|
77
86
|
build_path.mkdir(parents=True, exist_ok=True)
|
|
78
87
|
|
|
79
88
|
_check_initialized()
|
|
80
|
-
|
|
89
|
+
_clean_up_builds()
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
delete_placeholder_index_md_files()
|
|
92
|
+
create_placeholder_index_md_files()
|
|
93
|
+
build_metadata()
|
|
94
|
+
build_markdown()
|
|
95
|
+
build_jupyter_notebooks()
|
|
85
96
|
|
|
86
97
|
|
|
87
|
-
def postbuild():
|
|
98
|
+
def postbuild(dev_build: bool = False):
|
|
88
99
|
def _move_files(
|
|
89
100
|
source: Path,
|
|
90
101
|
destination: Path,
|
|
@@ -112,6 +123,9 @@ def postbuild():
|
|
|
112
123
|
shutil.move(str(source), str(destination))
|
|
113
124
|
print(f"Moved: {source} to {destination}")
|
|
114
125
|
|
|
126
|
+
build_content_json()
|
|
127
|
+
delete_placeholder_index_md_files()
|
|
128
|
+
|
|
115
129
|
website_static = ap.paths.website_path / "static"
|
|
116
130
|
website_static.mkdir(parents=True, exist_ok=True)
|
|
117
131
|
|
|
@@ -119,15 +133,24 @@ def postbuild():
|
|
|
119
133
|
myst_build = ap.paths.afterpython_path / content_type / "_build"
|
|
120
134
|
afterpython_content_build = ap.paths.build_path / content_type
|
|
121
135
|
# Move myst builds from afterpython/{content_type}/_build to afterpython/_build/{content_type}
|
|
122
|
-
_move_files(myst_build, afterpython_content_build, is_copy=
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
_move_files(myst_build, afterpython_content_build, is_copy=True)
|
|
137
|
+
# for production build, copy afterpython/_build/{content_type}/html to afterpython/_website/static/{content_type}
|
|
138
|
+
if not dev_build:
|
|
139
|
+
_move_files(
|
|
140
|
+
afterpython_content_build / "html",
|
|
141
|
+
website_static / content_type,
|
|
142
|
+
is_copy=True,
|
|
143
|
+
)
|
|
144
|
+
# for dev build, copy the files (mainly images, e.g. thumbnails) in site/public/ to afterpython/_website/static/{content_type}/build
|
|
145
|
+
else:
|
|
146
|
+
_move_files(
|
|
147
|
+
afterpython_content_build / "site" / "public",
|
|
148
|
+
# add build/ to mimic the structure of the production build
|
|
149
|
+
website_static / content_type / "build",
|
|
150
|
+
is_copy=True,
|
|
151
|
+
)
|
|
129
152
|
|
|
130
|
-
# Copy all files from afterpython/_build to afterpython/_website/static/
|
|
153
|
+
# Copy all files (e.g. metadata.json, blog.json etc.) from afterpython/_build to afterpython/_website/static/
|
|
131
154
|
_move_files(
|
|
132
155
|
ap.paths.build_path,
|
|
133
156
|
website_static,
|
|
@@ -150,16 +173,10 @@ def postbuild():
|
|
|
150
173
|
),
|
|
151
174
|
)
|
|
152
175
|
@click.pass_context
|
|
153
|
-
@click.option(
|
|
154
|
-
"--dev",
|
|
155
|
-
is_flag=True,
|
|
156
|
-
hidden=True, # Internal flag used by `ap dev`, not exposed to users
|
|
157
|
-
help="Development build - only build metadata for the landing page, skip content and production builds",
|
|
158
|
-
)
|
|
159
176
|
@click.option(
|
|
160
177
|
"--execute", is_flag=True, help="Execute Jupyter notebooks for all content types"
|
|
161
178
|
)
|
|
162
|
-
def build(ctx
|
|
179
|
+
def build(ctx: click.Context, execute: bool):
|
|
163
180
|
"""Build the project website and all contents for production.
|
|
164
181
|
|
|
165
182
|
This command builds MyST content (doc/blog/tutorial/example/guide) and the SvelteKit website.
|
|
@@ -169,7 +186,11 @@ def build(ctx, dev: bool, execute: bool):
|
|
|
169
186
|
|
|
170
187
|
Use --execute to execute Jupyter notebooks for all content types.
|
|
171
188
|
"""
|
|
172
|
-
from afterpython.utils import
|
|
189
|
+
from afterpython.utils import (
|
|
190
|
+
find_node_env,
|
|
191
|
+
handle_passthrough_help,
|
|
192
|
+
has_content_for_myst,
|
|
193
|
+
)
|
|
173
194
|
|
|
174
195
|
# Show both our options and myst's help and exit
|
|
175
196
|
handle_passthrough_help(
|
|
@@ -187,46 +208,53 @@ def build(ctx, dev: bool, execute: bool):
|
|
|
187
208
|
click.echo(f"Using BASE_PATH: {base_path}")
|
|
188
209
|
else:
|
|
189
210
|
click.echo("Using BASE_PATH: (empty - custom domain)")
|
|
190
|
-
|
|
191
|
-
click.echo("Building metadata.json...")
|
|
192
|
-
build_metadata()
|
|
211
|
+
node_env: NodeEnv = find_node_env()
|
|
193
212
|
|
|
194
213
|
# myst's production build
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
build_env = {**node_env, "BASE_URL": base_url, "BASE_PATH": base_path}
|
|
207
|
-
result = subprocess.run(
|
|
208
|
-
[
|
|
209
|
-
"myst",
|
|
210
|
-
"build",
|
|
211
|
-
"--html",
|
|
212
|
-
*(["--execute"] if execute else []),
|
|
213
|
-
*ctx.args,
|
|
214
|
-
],
|
|
215
|
-
cwd=content_path,
|
|
216
|
-
env=build_env,
|
|
217
|
-
check=False,
|
|
214
|
+
for content_type in CONTENT_TYPES:
|
|
215
|
+
content_path = ap.paths.afterpython_path / content_type
|
|
216
|
+
myst_yml_path = content_path / "myst.yml"
|
|
217
|
+
|
|
218
|
+
if not content_path.exists():
|
|
219
|
+
click.echo(f"{content_path} does not exist, skip building {content_type}/")
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
if not myst_yml_path.exists():
|
|
223
|
+
click.echo(
|
|
224
|
+
f"No myst.yml found in {content_path}, skip building {content_type}/"
|
|
218
225
|
)
|
|
219
|
-
|
|
220
|
-
raise Exit(result.returncode)
|
|
226
|
+
continue
|
|
221
227
|
|
|
222
|
-
|
|
228
|
+
if not has_content_for_myst(content_path):
|
|
229
|
+
click.echo(f"Skipping {content_type}/ (no content files found)")
|
|
230
|
+
continue
|
|
223
231
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
click.echo(f"Building {content_type}/...")
|
|
233
|
+
# NOTE: needs to set BASE_URL (used by MyST) so that the project website can link to the content pages correctly at e.g. localhost:5173/doc
|
|
234
|
+
base_url = f"{base_path}/{content_type}"
|
|
235
|
+
build_env = {**node_env, "BASE_URL": base_url, "BASE_PATH": base_path}
|
|
228
236
|
result = subprocess.run(
|
|
229
|
-
[
|
|
237
|
+
[
|
|
238
|
+
"myst",
|
|
239
|
+
"build",
|
|
240
|
+
"--html",
|
|
241
|
+
*(["--execute"] if execute else []),
|
|
242
|
+
*ctx.args,
|
|
243
|
+
],
|
|
244
|
+
cwd=content_path,
|
|
245
|
+
env=build_env,
|
|
246
|
+
check=False,
|
|
230
247
|
)
|
|
248
|
+
|
|
231
249
|
if result.returncode != 0:
|
|
232
250
|
raise Exit(result.returncode)
|
|
251
|
+
|
|
252
|
+
postbuild(dev_build=False)
|
|
253
|
+
|
|
254
|
+
click.echo("Building project website...")
|
|
255
|
+
website_env = {**node_env, "BASE_PATH": base_path}
|
|
256
|
+
result = subprocess.run(
|
|
257
|
+
["pnpm", "build"], cwd=paths.website_path, env=website_env, check=False
|
|
258
|
+
)
|
|
259
|
+
if result.returncode != 0:
|
|
260
|
+
raise Exit(result.returncode)
|
|
@@ -13,7 +13,7 @@ from click.exceptions import Exit
|
|
|
13
13
|
)
|
|
14
14
|
@click.pass_context
|
|
15
15
|
def check(ctx):
|
|
16
|
-
"""
|
|
16
|
+
"""Run ruff linter (uses afterpython/ruff.toml if available)"""
|
|
17
17
|
from afterpython.utils import handle_passthrough_help
|
|
18
18
|
|
|
19
19
|
# Show both our options and ruff's help and exit
|
|
@@ -26,10 +26,10 @@ from afterpython.utils import find_node_env, has_content_for_myst
|
|
|
26
26
|
@click.option(
|
|
27
27
|
"--all",
|
|
28
28
|
is_flag=True,
|
|
29
|
-
help=
|
|
29
|
+
help="Also clean AfterPython build directories (_build/ and _website/build/)",
|
|
30
30
|
)
|
|
31
31
|
def clean(ctx, all: bool):
|
|
32
|
-
"""Clean
|
|
32
|
+
"""Clean build artifacts from content types and website"""
|
|
33
33
|
from afterpython.utils import handle_passthrough_help
|
|
34
34
|
|
|
35
35
|
# Show both our options and myst's help and exit
|
|
@@ -13,7 +13,10 @@ import click
|
|
|
13
13
|
)
|
|
14
14
|
@click.pass_context
|
|
15
15
|
@click.option(
|
|
16
|
-
"--no-cz",
|
|
16
|
+
"--no-cz",
|
|
17
|
+
"--no-commitizen",
|
|
18
|
+
is_flag=True,
|
|
19
|
+
help="Skip 'commitizen' and 'commitizen-branch' pre-commit hooks",
|
|
17
20
|
)
|
|
18
21
|
@click.option(
|
|
19
22
|
"--no-pc",
|
|
@@ -22,7 +25,7 @@ import click
|
|
|
22
25
|
help="Skip running pre-commit checks before commit",
|
|
23
26
|
)
|
|
24
27
|
def commit(ctx, no_cz: bool, no_pc: bool):
|
|
25
|
-
"""
|
|
28
|
+
"""Create a conventional commit with interactive prompts"""
|
|
26
29
|
from afterpython.utils import handle_passthrough_help
|
|
27
30
|
|
|
28
31
|
# Show both our options and commitizen's help and exit
|
|
@@ -13,7 +13,7 @@ from click.exceptions import Exit
|
|
|
13
13
|
)
|
|
14
14
|
@click.pass_context
|
|
15
15
|
def commitizen(ctx):
|
|
16
|
-
"""Run commitizen"""
|
|
16
|
+
"""Run commitizen CLI (uses afterpython/cz.toml)"""
|
|
17
17
|
from afterpython.utils import handle_passthrough_help
|
|
18
18
|
|
|
19
19
|
# Show both our options and commitizen's help and exit
|
afterpython/cli/commands/dev.py
CHANGED
|
@@ -11,6 +11,7 @@ import time
|
|
|
11
11
|
import click
|
|
12
12
|
from click.exceptions import Exit
|
|
13
13
|
|
|
14
|
+
from afterpython.cli.commands.build import postbuild, prebuild
|
|
14
15
|
from afterpython.const import CONTENT_TYPES
|
|
15
16
|
from afterpython.utils import find_available_port, find_node_env
|
|
16
17
|
|
|
@@ -107,9 +108,6 @@ def dev(
|
|
|
107
108
|
|
|
108
109
|
paths = ctx.obj["paths"]
|
|
109
110
|
|
|
110
|
-
# OPTIMIZE: should implement incremental build?
|
|
111
|
-
subprocess.run(["ap", "build", "--dev"])
|
|
112
|
-
|
|
113
111
|
def cleanup_processes():
|
|
114
112
|
"""Clean up all MyST server processes"""
|
|
115
113
|
click.echo("\nShutting down MyST servers...")
|
|
@@ -140,11 +138,14 @@ def dev(
|
|
|
140
138
|
enabled_content_types = {ct for ct, flag in content_flags.items() if flag}
|
|
141
139
|
|
|
142
140
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
prebuild()
|
|
142
|
+
|
|
143
|
+
# Clear .env.development before writing new ports
|
|
144
|
+
env_file = paths.website_path / ".env.development"
|
|
145
|
+
env_file.write_text("") # Clear existing content
|
|
147
146
|
|
|
147
|
+
# myst development servers
|
|
148
|
+
if enabled_content_types:
|
|
148
149
|
next_port = 3000
|
|
149
150
|
for content_type in CONTENT_TYPES:
|
|
150
151
|
# Skip content types that are not enabled
|
|
@@ -186,6 +187,8 @@ def dev(
|
|
|
186
187
|
# causing "address already in use" errors.
|
|
187
188
|
time.sleep(3)
|
|
188
189
|
|
|
190
|
+
postbuild(dev_build=True)
|
|
191
|
+
|
|
189
192
|
if not no_website:
|
|
190
193
|
node_env: NodeEnv = find_node_env()
|
|
191
194
|
click.echo("Running the web dev server...")
|
|
@@ -13,7 +13,7 @@ from click.exceptions import Exit
|
|
|
13
13
|
)
|
|
14
14
|
@click.pass_context
|
|
15
15
|
def format(ctx):
|
|
16
|
-
"""
|
|
16
|
+
"""Run ruff formatter"""
|
|
17
17
|
from afterpython.utils import handle_passthrough_help
|
|
18
18
|
|
|
19
19
|
# Show both our options and ruff's help and exit
|
afterpython/cli/commands/init.py
CHANGED
|
@@ -48,7 +48,7 @@ def init_py_typed():
|
|
|
48
48
|
)
|
|
49
49
|
@click.pass_context
|
|
50
50
|
def init(ctx, yes):
|
|
51
|
-
"""Initialize
|
|
51
|
+
"""Initialize AfterPython project structure and website template"""
|
|
52
52
|
from afterpython.tools._afterpython import init_afterpython
|
|
53
53
|
from afterpython.tools.commitizen import init_commitizen
|
|
54
54
|
from afterpython.tools.github_actions import (
|
|
@@ -13,7 +13,7 @@ from afterpython.utils import has_pixi, has_uv
|
|
|
13
13
|
)
|
|
14
14
|
)
|
|
15
15
|
def install():
|
|
16
|
-
"""Install all dependencies (
|
|
16
|
+
"""Install all project dependencies (manages both uv and pixi if present)"""
|
|
17
17
|
if not has_uv():
|
|
18
18
|
click.echo("uv not found. Please install uv first.")
|
|
19
19
|
return
|
afterpython/cli/commands/lock.py
CHANGED
|
@@ -8,7 +8,7 @@ from afterpython.utils import has_pixi, has_uv
|
|
|
8
8
|
|
|
9
9
|
@click.command()
|
|
10
10
|
def lock():
|
|
11
|
-
"""Lock
|
|
11
|
+
"""Lock project dependencies (manages both uv and pixi if present)"""
|
|
12
12
|
if not has_uv():
|
|
13
13
|
click.echo("uv not found. Please install uv first.")
|
|
14
14
|
return
|
|
@@ -27,7 +27,7 @@ def _command_supports_config(command: str) -> bool:
|
|
|
27
27
|
)
|
|
28
28
|
@click.pass_context
|
|
29
29
|
def pre_commit(ctx):
|
|
30
|
-
"""Run pre-commit
|
|
30
|
+
"""Run pre-commit hooks (uses afterpython/.pre-commit-config.yaml if available)"""
|
|
31
31
|
from afterpython.utils import handle_passthrough_help
|
|
32
32
|
|
|
33
33
|
# Show both our options and pre-commit's help and exit
|