afterpython 0.3.13__tar.gz → 0.3.14__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.3.13 → afterpython-0.3.14}/PKG-INFO +1 -1
- {afterpython-0.3.13 → afterpython-0.3.14}/pyproject.toml +1 -1
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/content_json.py +20 -12
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/dev.py +108 -12
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/sync.py +10 -5
- {afterpython-0.3.13 → afterpython-0.3.14}/README.md +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/__init__.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/_io/toml.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/_io/yaml.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/_paths.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/_typing.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/__init__.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/build_pdoc.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/faq_json.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/index_md.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/jupyter_notebook.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/llms_txt.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/marimo_notebook.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/markdown.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/metadata.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/builders/url_md.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/__init__.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/add.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/build.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/bump.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/check.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/clean.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/commit.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/commitizen.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/format.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/init.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/init_branch_rules.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/install.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/lock.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/pre_commit.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/preview.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/release.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/remove.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/serve.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/start.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/commands/update.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/cli/main.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/const.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/main.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/pcu.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/py.typed +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/ci-workflow-template.yml +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/cz-template.toml +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/dependabot-template.yml +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/deploy-workflow-template.yml +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/pdoc/frame.html.jinja2 +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/pre-commit-config-template.yaml +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/release-workflow-template.yml +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/ruff-template.toml +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/tools/_afterpython.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/tools/_git.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/tools/branch_rules.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/tools/commitizen.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/tools/github_actions.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/tools/myst.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/tools/pre_commit.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/tools/pyproject.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/utils.py +0 -0
- {afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: afterpython
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.14
|
|
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
|
|
@@ -37,19 +37,23 @@ def standardize_data(content_type: tContentType, content: dict) -> dict:
|
|
|
37
37
|
is_featured = content["slug"] == featured_post or content["location"].endswith(
|
|
38
38
|
featured_post
|
|
39
39
|
)
|
|
40
|
+
# `frontmatter.get(key, default)` only falls back when the key is missing —
|
|
41
|
+
# but YAML parses `thumbnail:` (empty value) as None, which then propagates
|
|
42
|
+
# into convert_paths() and crashes the string concatenation. Coerce here
|
|
43
|
+
# so downstream sees "" / [] consistently.
|
|
40
44
|
return {
|
|
41
45
|
"version": content["version"],
|
|
42
46
|
"slug": content["slug"],
|
|
43
47
|
"location": content["location"],
|
|
44
48
|
"featured": is_featured,
|
|
45
49
|
# frontmatter fields
|
|
46
|
-
"title": frontmatter.get("title"
|
|
47
|
-
"description": frontmatter.get("description"
|
|
48
|
-
"tags": frontmatter.get("tags"
|
|
49
|
-
"date": frontmatter.get("date"
|
|
50
|
-
"thumbnail": frontmatter.get("thumbnail"
|
|
51
|
-
"thumbnailOptimized": frontmatter.get("thumbnailOptimized"
|
|
52
|
-
"authors": frontmatter.get("authors"
|
|
50
|
+
"title": frontmatter.get("title") or "",
|
|
51
|
+
"description": frontmatter.get("description") or "",
|
|
52
|
+
"tags": frontmatter.get("tags") or [],
|
|
53
|
+
"date": frontmatter.get("date") or "",
|
|
54
|
+
"thumbnail": frontmatter.get("thumbnail") or "",
|
|
55
|
+
"thumbnailOptimized": frontmatter.get("thumbnailOptimized") or "",
|
|
56
|
+
"authors": frontmatter.get("authors") or [],
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
|
|
@@ -63,11 +67,15 @@ def convert_paths(content_type: tContentType, output_file: Path):
|
|
|
63
67
|
|
|
64
68
|
for item in content:
|
|
65
69
|
if not item.get("thumbnailOptimized"):
|
|
66
|
-
item["thumbnailOptimized"] = item
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
item["thumbnailOptimized"] = item.get("thumbnail") or ""
|
|
71
|
+
# Skip path prefixing when there's no thumbnail — prepending to "" would
|
|
72
|
+
# produce a bogus "/blog/build" URL that 404s in the frontend.
|
|
73
|
+
if item.get("thumbnail"):
|
|
74
|
+
item["thumbnail"] = f"/{content_type}/build" + item["thumbnail"]
|
|
75
|
+
if item.get("thumbnailOptimized"):
|
|
76
|
+
item["thumbnailOptimized"] = (
|
|
77
|
+
f"/{content_type}/build" + item["thumbnailOptimized"]
|
|
78
|
+
)
|
|
71
79
|
|
|
72
80
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
73
81
|
json.dump(content, f, indent=2, ensure_ascii=False)
|
|
@@ -5,7 +5,12 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from afterpython._typing import NodeEnv
|
|
7
7
|
|
|
8
|
+
import contextlib
|
|
9
|
+
import os
|
|
10
|
+
import queue
|
|
11
|
+
import signal
|
|
8
12
|
import subprocess
|
|
13
|
+
import threading
|
|
9
14
|
import time
|
|
10
15
|
|
|
11
16
|
import click
|
|
@@ -16,6 +21,76 @@ from afterpython.const import CONTENT_TYPES
|
|
|
16
21
|
from afterpython.utils import find_available_port, find_node_env
|
|
17
22
|
|
|
18
23
|
|
|
24
|
+
def _stream_process_output(
|
|
25
|
+
proc: subprocess.Popen,
|
|
26
|
+
output_queue: queue.Queue[str | None],
|
|
27
|
+
ready_event: threading.Event,
|
|
28
|
+
):
|
|
29
|
+
"""Forward process output to the terminal and queue startup lines."""
|
|
30
|
+
if proc.stdout is None:
|
|
31
|
+
if not ready_event.is_set():
|
|
32
|
+
output_queue.put(None)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
for line in proc.stdout:
|
|
36
|
+
click.echo(line, nl=False)
|
|
37
|
+
if not ready_event.is_set():
|
|
38
|
+
output_queue.put(line)
|
|
39
|
+
|
|
40
|
+
if not ready_event.is_set():
|
|
41
|
+
output_queue.put(None)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _wait_for_myst_server(
|
|
45
|
+
proc: subprocess.Popen,
|
|
46
|
+
content_type: str,
|
|
47
|
+
port: int,
|
|
48
|
+
output_queue: queue.Queue[str | None],
|
|
49
|
+
ready_event: threading.Event,
|
|
50
|
+
timeout: int = 300,
|
|
51
|
+
):
|
|
52
|
+
"""Wait until MyST reports that its dev server has started."""
|
|
53
|
+
deadline = time.monotonic() + timeout
|
|
54
|
+
ready_markers = (
|
|
55
|
+
f"Server started on port {port}",
|
|
56
|
+
f"http://localhost:{port}",
|
|
57
|
+
f"http://127.0.0.1:{port}",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
while time.monotonic() < deadline:
|
|
62
|
+
returncode = proc.poll()
|
|
63
|
+
if returncode is not None and output_queue.empty():
|
|
64
|
+
if returncode == 0:
|
|
65
|
+
click.echo(
|
|
66
|
+
f"MyST {content_type} server exited before becoming ready"
|
|
67
|
+
)
|
|
68
|
+
return
|
|
69
|
+
raise Exit(returncode)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
line = output_queue.get(timeout=0.5)
|
|
73
|
+
except queue.Empty:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
if line is None:
|
|
77
|
+
returncode = proc.poll()
|
|
78
|
+
if returncode not in (None, 0):
|
|
79
|
+
raise Exit(returncode)
|
|
80
|
+
click.echo(f"MyST {content_type} server exited before becoming ready")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if any(marker in line for marker in ready_markers):
|
|
84
|
+
click.echo(f"MyST {content_type} server is ready")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
raise click.ClickException(
|
|
88
|
+
f"Timed out waiting for MyST {content_type} server on port {port}"
|
|
89
|
+
)
|
|
90
|
+
finally:
|
|
91
|
+
ready_event.set()
|
|
92
|
+
|
|
93
|
+
|
|
19
94
|
@click.command(
|
|
20
95
|
add_help_option=False, # disable click's --help option so that ap dev --help can work
|
|
21
96
|
context_settings=dict(
|
|
@@ -116,15 +191,26 @@ def dev(
|
|
|
116
191
|
paths = ctx.obj["paths"]
|
|
117
192
|
|
|
118
193
|
def cleanup_processes():
|
|
119
|
-
"""Clean up all MyST server processes
|
|
194
|
+
"""Clean up all MyST server processes.
|
|
195
|
+
|
|
196
|
+
Each spawned 'ap {content_type}' is its own session leader (start_new_session=True
|
|
197
|
+
below), so its grandchild `myst start` shares the same process group. proc.terminate()
|
|
198
|
+
would only SIGTERM the outer Python wrapper — which is blocked in subprocess.run and
|
|
199
|
+
doesn't forward the signal — leaving myst orphaned. Signal the whole group instead.
|
|
200
|
+
"""
|
|
120
201
|
click.echo("\nShutting down MyST servers...")
|
|
121
202
|
for proc in myst_processes:
|
|
122
203
|
try:
|
|
123
|
-
proc.
|
|
204
|
+
pgid = os.getpgid(proc.pid)
|
|
205
|
+
except ProcessLookupError:
|
|
206
|
+
continue
|
|
207
|
+
try:
|
|
208
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
124
209
|
proc.wait(timeout=5)
|
|
125
210
|
except subprocess.TimeoutExpired:
|
|
126
|
-
|
|
127
|
-
|
|
211
|
+
with contextlib.suppress(ProcessLookupError):
|
|
212
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
213
|
+
except ProcessLookupError:
|
|
128
214
|
pass
|
|
129
215
|
|
|
130
216
|
# Determine which content types to run
|
|
@@ -175,6 +261,8 @@ def dev(
|
|
|
175
261
|
f"PUBLIC_{content_type.upper()}_URL=http://localhost:{myst_port}\n"
|
|
176
262
|
)
|
|
177
263
|
|
|
264
|
+
output_queue: queue.Queue[str | None] = queue.Queue()
|
|
265
|
+
ready_event = threading.Event()
|
|
178
266
|
myst_process = subprocess.Popen(
|
|
179
267
|
[
|
|
180
268
|
"ap",
|
|
@@ -184,15 +272,23 @@ def dev(
|
|
|
184
272
|
*(["--execute"] if execute else []),
|
|
185
273
|
*ctx.args,
|
|
186
274
|
],
|
|
187
|
-
#
|
|
188
|
-
#
|
|
275
|
+
# New session so cleanup_processes can SIGTERM the whole group and
|
|
276
|
+
# take the grandchild `myst start` down with the wrapper.
|
|
277
|
+
start_new_session=True,
|
|
278
|
+
stdout=subprocess.PIPE,
|
|
279
|
+
stderr=subprocess.STDOUT,
|
|
280
|
+
text=True,
|
|
281
|
+
bufsize=1,
|
|
189
282
|
)
|
|
190
283
|
myst_processes.append(myst_process)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
284
|
+
threading.Thread(
|
|
285
|
+
target=_stream_process_output,
|
|
286
|
+
args=(myst_process, output_queue, ready_event),
|
|
287
|
+
daemon=True,
|
|
288
|
+
).start()
|
|
289
|
+
_wait_for_myst_server(
|
|
290
|
+
myst_process, content_type, myst_port, output_queue, ready_event
|
|
291
|
+
)
|
|
196
292
|
|
|
197
293
|
postbuild(dev_build=True)
|
|
198
294
|
|
|
@@ -208,7 +304,7 @@ def dev(
|
|
|
208
304
|
click.echo(
|
|
209
305
|
"Skipping website dev server (--no-website flag). Run 'pnpm dev' manually in afterpython/_website/ with your custom options."
|
|
210
306
|
)
|
|
211
|
-
if
|
|
307
|
+
if myst_processes:
|
|
212
308
|
# Keep the process running to maintain MyST servers
|
|
213
309
|
click.echo("Press Ctrl+C to stop MyST servers...")
|
|
214
310
|
while True:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from pathlib import PurePosixPath
|
|
2
3
|
|
|
3
4
|
import click
|
|
4
5
|
from pyproject_metadata import StandardMetadata
|
|
@@ -8,6 +9,13 @@ import afterpython as ap
|
|
|
8
9
|
from afterpython.utils import convert_author_name_to_id, normalize_static_path
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
def _myst_favicon_path(path: str) -> str:
|
|
13
|
+
"""Convert a website favicon path to MyST's required .ico favicon path."""
|
|
14
|
+
if not path:
|
|
15
|
+
return ""
|
|
16
|
+
return str(PurePosixPath(path).with_suffix(".ico"))
|
|
17
|
+
|
|
18
|
+
|
|
11
19
|
def _sync_authors_yml(authors: list[tuple[str, str | None]]):
|
|
12
20
|
"""Sync authors.yml with authors in pyproject.toml"""
|
|
13
21
|
from afterpython._io.yaml import read_yaml
|
|
@@ -58,10 +66,7 @@ def sync():
|
|
|
58
66
|
website_favicon = normalize_static_path(
|
|
59
67
|
str(_from_tomlkit(afterpython.get("website", {})).get("favicon", ""))
|
|
60
68
|
)
|
|
61
|
-
|
|
62
|
-
raise ValueError(
|
|
63
|
-
f"Invalid favicon '{website_favicon}': mystmd only supports .ico files."
|
|
64
|
-
)
|
|
69
|
+
myst_favicon = _myst_favicon_path(website_favicon)
|
|
65
70
|
website_logo = normalize_static_path(
|
|
66
71
|
str(_from_tomlkit(afterpython.get("website", {})).get("logo", ""))
|
|
67
72
|
)
|
|
@@ -133,7 +138,7 @@ def sync():
|
|
|
133
138
|
"site": {
|
|
134
139
|
"title": title,
|
|
135
140
|
"options": {
|
|
136
|
-
"favicon": "../static" +
|
|
141
|
+
"favicon": "../static" + myst_favicon if myst_favicon else "",
|
|
137
142
|
"logo": "../static" + website_logo if website_logo else "",
|
|
138
143
|
"logo_dark": "../static" + website_logo_dark
|
|
139
144
|
if website_logo_dark
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/ci-workflow-template.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/deploy-workflow-template.yml
RENAMED
|
File without changes
|
|
File without changes
|
{afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/pre-commit-config-template.yaml
RENAMED
|
File without changes
|
{afterpython-0.3.13 → afterpython-0.3.14}/src/afterpython/templates/release-workflow-template.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|