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.
Files changed (33) hide show
  1. afterpython/builders/__init__.py +12 -2
  2. afterpython/builders/content_json.py +131 -0
  3. afterpython/builders/index_md.py +94 -0
  4. afterpython/builders/jupyter_notebook.py +293 -0
  5. afterpython/builders/markdown.py +271 -0
  6. afterpython/builders/metadata.py +5 -1
  7. afterpython/cli/commands/build.py +90 -62
  8. afterpython/cli/commands/check.py +1 -1
  9. afterpython/cli/commands/clean.py +2 -2
  10. afterpython/cli/commands/commit.py +5 -2
  11. afterpython/cli/commands/commitizen.py +1 -1
  12. afterpython/cli/commands/dev.py +10 -7
  13. afterpython/cli/commands/format.py +1 -1
  14. afterpython/cli/commands/init.py +1 -1
  15. afterpython/cli/commands/install.py +1 -1
  16. afterpython/cli/commands/lock.py +1 -1
  17. afterpython/cli/commands/pre_commit.py +1 -1
  18. afterpython/cli/commands/preview.py +1 -1
  19. afterpython/cli/commands/start.py +1 -1
  20. afterpython/cli/commands/sync.py +33 -6
  21. afterpython/cli/commands/update.py +5 -5
  22. afterpython/templates/ci-workflow-template.yml +1 -1
  23. afterpython/templates/cz-template.toml +0 -1
  24. afterpython/templates/pre-commit-config-template.yaml +1 -1
  25. afterpython/tools/_afterpython.py +71 -9
  26. afterpython/tools/myst.py +42 -3
  27. afterpython/utils.py +73 -0
  28. {afterpython-0.1.0.dev17.dist-info → afterpython-0.1.1.dist-info}/METADATA +14 -9
  29. afterpython-0.1.1.dist-info/RECORD +57 -0
  30. {afterpython-0.1.0.dev17.dist-info → afterpython-0.1.1.dist-info}/WHEEL +1 -1
  31. afterpython/builders/molab.py +0 -100
  32. afterpython-0.1.0.dev17.dist-info/RECORD +0 -54
  33. {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 (including myst's builds)"""
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
- """Simple wrapper for `myst start` for convenience"""
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,
@@ -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+afterpython.toml and authors.yml+myst.yml files"""
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
- item for item in nav_bar if item["title"] != content_type.capitalize() + "s"
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
- "nav": nav_bar_per_content_type,
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="if enabled, the dependencies will be upgraded to the latest version",
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 all versions in dependencies and tool configurations (e.g. pre-commit-config.yaml)",
32
+ help="Also update pre-commit hooks and pixi dependencies",
33
33
  )
34
34
  def dependencies(upgrade: bool, all: bool):
35
- """Update pyproject.toml dependencies to the latest version"""
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="if enabled, the existing project website template will not be backed up",
125
+ help="Skip creating a backup of the existing website template",
126
126
  )
127
127
  def website(ctx, no_backup: bool):
128
- """Update the project website template to the latest version"""
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"
@@ -118,7 +118,7 @@ jobs:
118
118
  - name: Set up pixi
119
119
  uses: prefix-dev/setup-pixi@v0.9.3
120
120
  with:
121
- pixi-version: v0.59.0
121
+ pixi-version: v0.60.0
122
122
  cache: true
123
123
  # Optional: Uncomment if you need prefix.dev authentication
124
124
  # auth-host: prefix.dev
@@ -1,6 +1,5 @@
1
1
  [tool.commitizen]
2
2
  name = "cz_conventional_commits"
3
- # name = "cz_customize"
4
3
  tag_format = "v$version"
5
4
  version_scheme = "pep440"
6
5
  version_provider = "uv"
@@ -23,7 +23,7 @@ repos:
23
23
  stages:
24
24
  - pre-commit
25
25
  - repo: https://github.com/astral-sh/ruff-pre-commit
26
- rev: v0.14.6
26
+ rev: v0.14.8
27
27
  hooks:
28
28
  - id: ruff-check
29
29
  stages:
@@ -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
- update_afterpython(
59
- {
60
- "company": {
61
- "name": "",
62
- "url": "",
63
- },
64
- "website": {"url": ""},
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.0.dev17
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.6.6
25
+ Requires-Dist: mystmd>=1.7.0
27
26
  Requires-Dist: pdoc>=16.0.0
28
- Requires-Dist: ruff>=0.14.6
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.13
2
+ Generator: uv 0.9.16
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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"[![Open in molab]({_get_molab_badge()})]({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)