marimo-dev 0.1.14__tar.gz → 0.2.2__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.
- {marimo_dev-0.1.14 → marimo_dev-0.2.2}/PKG-INFO +14 -6
- {marimo_dev-0.1.14 → marimo_dev-0.2.2}/README.md +7 -5
- {marimo_dev-0.1.14 → marimo_dev-0.2.2}/pyproject.toml +9 -2
- {marimo_dev-0.1.14 → marimo_dev-0.2.2}/src/marimo_dev/__init__.py +18 -5
- marimo_dev-0.2.2/src/marimo_dev/build.py +98 -0
- {marimo_dev-0.1.14 → marimo_dev-0.2.2}/src/marimo_dev/core.py +1 -0
- marimo_dev-0.2.2/src/marimo_dev/docs.py +238 -0
- {marimo_dev-0.1.14 → marimo_dev-0.2.2}/src/marimo_dev/read.py +34 -8
- marimo_dev-0.1.14/src/marimo_dev/build.py +0 -40
- marimo_dev-0.1.14/src/marimo_dev/cli.py +0 -18
- marimo_dev-0.1.14/src/marimo_dev/docs.py +0 -58
- {marimo_dev-0.1.14 → marimo_dev-0.2.2}/src/marimo_dev/pkg.py +0 -0
- {marimo_dev-0.1.14 → marimo_dev-0.2.2}/src/marimo_dev/publish.py +0 -0
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: marimo-dev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Build and publish python packages from marimo notebooks
|
|
5
5
|
Author: Deufel
|
|
6
6
|
Author-email: Deufel <MDeufel13@gmail.com>
|
|
7
7
|
License: MIT
|
|
8
8
|
Requires-Dist: fastcore>=1.10.0
|
|
9
|
+
Requires-Dist: ipython>=9.8.0
|
|
10
|
+
Requires-Dist: marimo>=0.19.7
|
|
11
|
+
Requires-Dist: mohtml>=0.1.11
|
|
12
|
+
Requires-Dist: pydantic-ai-slim>=1.50.0
|
|
13
|
+
Requires-Dist: pytest>=9.0.2
|
|
9
14
|
Requires-Dist: python-fasthtml>=0.12.37
|
|
10
15
|
Requires-Dist: youtube-transcript-api>=1.2.3
|
|
11
16
|
Requires-Python: >=3.12
|
|
12
17
|
Project-URL: Repository, https://github.com/deufel/m-dev
|
|
18
|
+
Project-URL: PyPI, https://pypi.org/project/marimo-dev/
|
|
13
19
|
Description-Content-Type: text/markdown
|
|
14
20
|
|
|
15
21
|
|
|
@@ -87,7 +93,8 @@ from .core import greet
|
|
|
87
93
|
|
|
88
94
|
## What gets exported
|
|
89
95
|
|
|
90
|
-
|
|
96
|
+
1. constants in setup cells
|
|
97
|
+
2. [self-contained functions and classes](https://docs.marimo.io/guides/reusing_functions/)
|
|
91
98
|
|
|
92
99
|
## Hash pipe directives
|
|
93
100
|
|
|
@@ -101,12 +108,12 @@ def helper():
|
|
|
101
108
|
|
|
102
109
|
@app.function
|
|
103
110
|
#| internal
|
|
104
|
-
def
|
|
111
|
+
def private():
|
|
105
112
|
pass # not added to __all__
|
|
106
113
|
|
|
107
114
|
@app.function
|
|
108
115
|
#| nodoc internal
|
|
109
|
-
def
|
|
116
|
+
def helper():
|
|
110
117
|
pass # neither exported nor documented
|
|
111
118
|
```
|
|
112
119
|
|
|
@@ -142,13 +149,14 @@ skip_prefixes = ["XX_", "test_"] # ignore these files
|
|
|
142
149
|
## Commands
|
|
143
150
|
|
|
144
151
|
```bash
|
|
145
|
-
md build # build package from notebooks
|
|
152
|
+
md build # build package from notebooks and make docs
|
|
153
|
+
md docs # build the static docs (beta)
|
|
146
154
|
md publish --test # publish to Test PyPI
|
|
147
155
|
md publish # publish to PyPI
|
|
148
156
|
md tidy # remove __pycache__ and cache files
|
|
149
157
|
md nuke # remove all build artifacts (dist, docs, src, temp*)
|
|
150
158
|
```
|
|
151
|
-
If you make a temp folder it will be explicitly removed when running `md nuke
|
|
159
|
+
*If you make a temp folder it will be explicitly removed when running `md nuke`*
|
|
152
160
|
|
|
153
161
|
## Dependencies
|
|
154
162
|
|
|
@@ -73,7 +73,8 @@ from .core import greet
|
|
|
73
73
|
|
|
74
74
|
## What gets exported
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
1. constants in setup cells
|
|
77
|
+
2. [self-contained functions and classes](https://docs.marimo.io/guides/reusing_functions/)
|
|
77
78
|
|
|
78
79
|
## Hash pipe directives
|
|
79
80
|
|
|
@@ -87,12 +88,12 @@ def helper():
|
|
|
87
88
|
|
|
88
89
|
@app.function
|
|
89
90
|
#| internal
|
|
90
|
-
def
|
|
91
|
+
def private():
|
|
91
92
|
pass # not added to __all__
|
|
92
93
|
|
|
93
94
|
@app.function
|
|
94
95
|
#| nodoc internal
|
|
95
|
-
def
|
|
96
|
+
def helper():
|
|
96
97
|
pass # neither exported nor documented
|
|
97
98
|
```
|
|
98
99
|
|
|
@@ -128,13 +129,14 @@ skip_prefixes = ["XX_", "test_"] # ignore these files
|
|
|
128
129
|
## Commands
|
|
129
130
|
|
|
130
131
|
```bash
|
|
131
|
-
md build # build package from notebooks
|
|
132
|
+
md build # build package from notebooks and make docs
|
|
133
|
+
md docs # build the static docs (beta)
|
|
132
134
|
md publish --test # publish to Test PyPI
|
|
133
135
|
md publish # publish to PyPI
|
|
134
136
|
md tidy # remove __pycache__ and cache files
|
|
135
137
|
md nuke # remove all build artifacts (dist, docs, src, temp*)
|
|
136
138
|
```
|
|
137
|
-
If you make a temp folder it will be explicitly removed when running `md nuke
|
|
139
|
+
*If you make a temp folder it will be explicitly removed when running `md nuke`*
|
|
138
140
|
|
|
139
141
|
## Dependencies
|
|
140
142
|
|
|
@@ -4,12 +4,17 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "marimo-dev"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Build and publish python packages from marimo notebooks"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"fastcore>=1.10.0",
|
|
13
|
+
"ipython>=9.8.0",
|
|
14
|
+
"marimo>=0.19.7",
|
|
15
|
+
"mohtml>=0.1.11",
|
|
16
|
+
"pydantic-ai-slim>=1.50.0",
|
|
17
|
+
"pytest>=9.0.2",
|
|
13
18
|
"python-fasthtml>=0.12.37",
|
|
14
19
|
"youtube-transcript-api>=1.2.3",
|
|
15
20
|
]
|
|
@@ -34,6 +39,8 @@ classifiers = [
|
|
|
34
39
|
|
|
35
40
|
[project.urls]
|
|
36
41
|
Repository = "https://github.com/deufel/m-dev"
|
|
42
|
+
PyPI = "https://pypi.org/project/marimo-dev/"
|
|
43
|
+
|
|
37
44
|
|
|
38
45
|
# Marimo-Dev settings
|
|
39
46
|
[tool.marimo-dev]
|
|
@@ -44,7 +51,7 @@ Repository = "https://github.com/deufel/m-dev"
|
|
|
44
51
|
skip_prefixes = ["XX_", "test_"]
|
|
45
52
|
|
|
46
53
|
# Remove this when ready to upload this is a saftey net for not accidentally uploading
|
|
47
|
-
|
|
54
|
+
classifiers = ["Private :: Do Not Upload"]
|
|
48
55
|
|
|
49
56
|
# Development packages
|
|
50
57
|
[dependency-groups]
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
"""Build and publish python packages from marimo notebooks"""
|
|
2
|
-
__version__ = '0.
|
|
2
|
+
__version__ = '0.2.2'
|
|
3
3
|
__author__ = 'Deufel'
|
|
4
4
|
from .core import Config, read_config, Kind, Param, Node
|
|
5
5
|
from .read import inline_doc, parse_params, parse_hash_pipe, parse_class_params, parse_class_methods, parse_ret, src_with_decs, is_export, parse_import, parse_const, parse_export, parse_node, parse_file, read_meta, nb_name, scan
|
|
6
6
|
from .pkg import clean, write, write_mod, rewrite_imports, write_init
|
|
7
|
-
from .docs import cls_sig, fn_sig, sig, write_llms
|
|
8
|
-
from .build import build, tidy, nuke
|
|
7
|
+
from .docs import cls_sig, fn_sig, sig, write_llms, exp_type, render_param, nb_path, render_node, render_module_page, build_docs, export_wasm, write_nojekyll, html_preview, render_index_page, Icon
|
|
8
|
+
from .build import build, tidy, nuke, get_pypi_name, extract_import_names, pep723_header, bundle
|
|
9
9
|
from .publish import publish
|
|
10
|
-
from .cli import main
|
|
11
10
|
__all__ = [
|
|
12
11
|
"Config",
|
|
12
|
+
"Icon",
|
|
13
13
|
"Kind",
|
|
14
14
|
"Node",
|
|
15
15
|
"Param",
|
|
16
16
|
"build",
|
|
17
|
+
"build_docs",
|
|
18
|
+
"bundle",
|
|
17
19
|
"clean",
|
|
18
20
|
"cls_sig",
|
|
21
|
+
"exp_type",
|
|
22
|
+
"export_wasm",
|
|
23
|
+
"extract_import_names",
|
|
19
24
|
"fn_sig",
|
|
25
|
+
"get_pypi_name",
|
|
26
|
+
"html_preview",
|
|
20
27
|
"inline_doc",
|
|
21
28
|
"is_export",
|
|
22
|
-
"main",
|
|
23
29
|
"nb_name",
|
|
30
|
+
"nb_path",
|
|
24
31
|
"nuke",
|
|
25
32
|
"parse_class_methods",
|
|
26
33
|
"parse_class_params",
|
|
@@ -32,9 +39,14 @@ __all__ = [
|
|
|
32
39
|
"parse_node",
|
|
33
40
|
"parse_params",
|
|
34
41
|
"parse_ret",
|
|
42
|
+
"pep723_header",
|
|
35
43
|
"publish",
|
|
36
44
|
"read_config",
|
|
37
45
|
"read_meta",
|
|
46
|
+
"render_index_page",
|
|
47
|
+
"render_module_page",
|
|
48
|
+
"render_node",
|
|
49
|
+
"render_param",
|
|
38
50
|
"rewrite_imports",
|
|
39
51
|
"scan",
|
|
40
52
|
"sig",
|
|
@@ -44,4 +56,5 @@ __all__ = [
|
|
|
44
56
|
"write_init",
|
|
45
57
|
"write_llms",
|
|
46
58
|
"write_mod",
|
|
59
|
+
"write_nojekyll",
|
|
47
60
|
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from .core import Kind, Param, Node, Config, read_config
|
|
2
|
+
from .read import scan, read_meta
|
|
3
|
+
from .pkg import write_mod, write_init, clean
|
|
4
|
+
from .docs import write_llms
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import ast, shutil, re
|
|
7
|
+
|
|
8
|
+
IMPORT_TO_PYPI = {'bs4': 'beautifulsoup4', 'PIL': 'pillow', 'cv2': 'opencv-python', 'sklearn': 'scikit-learn', 'yaml': 'pyyaml'}
|
|
9
|
+
|
|
10
|
+
def build(
|
|
11
|
+
root='.', # root directory containing pyproject.toml
|
|
12
|
+
)->str: # path to built package
|
|
13
|
+
"Build a Python package from notebooks."
|
|
14
|
+
cfg = read_config(root)
|
|
15
|
+
meta, mods = scan(root)
|
|
16
|
+
mod_names = [name for name, _ in mods]
|
|
17
|
+
pkg = Path(root) / cfg.out / meta['name'].replace('-', '_')
|
|
18
|
+
if pkg.exists(): shutil.rmtree(pkg)
|
|
19
|
+
pkg.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
for name, nodes in mods:
|
|
21
|
+
stripped = re.sub(r'^[a-z]_', '', name)
|
|
22
|
+
if stripped != 'index' and any(n.kind == Kind.EXP for n in nodes): write_mod(pkg/f'{stripped}.py', nodes, mod_names)
|
|
23
|
+
write_init(pkg/'__init__.py', meta, mods)
|
|
24
|
+
all_exp = [n for _, nodes in mods for n in nodes if n.kind == Kind.EXP]
|
|
25
|
+
if all_exp: write_llms(meta, all_exp)
|
|
26
|
+
return str(pkg)
|
|
27
|
+
|
|
28
|
+
def tidy():
|
|
29
|
+
"Remove cache and temporary files (__pycache__, __marimo__, .pytest_cache, etc)."
|
|
30
|
+
import shutil
|
|
31
|
+
for p in Path('.').rglob('__pycache__'): shutil.rmtree(p, ignore_errors=True)
|
|
32
|
+
for p in Path('.').rglob('__marimo__'): shutil.rmtree(p, ignore_errors=True)
|
|
33
|
+
for p in Path('.').rglob('.pytest_cache'): shutil.rmtree(p, ignore_errors=True)
|
|
34
|
+
for p in Path('.').rglob('*.pyc'): p.unlink(missing_ok=True)
|
|
35
|
+
print("Cleaned cache files")
|
|
36
|
+
|
|
37
|
+
def nuke():
|
|
38
|
+
"Remove all build artifacts (dist, docs, src) and cache files."
|
|
39
|
+
import shutil
|
|
40
|
+
tidy()
|
|
41
|
+
for d in ['dist', 'docs', 'src', 'temp']: shutil.rmtree(d, ignore_errors=True)
|
|
42
|
+
print("Nuked build artifacts")
|
|
43
|
+
|
|
44
|
+
def get_pypi_name(import_name):
|
|
45
|
+
"Map import name to PyPI package name."
|
|
46
|
+
root = import_name.split('.')[0]
|
|
47
|
+
return IMPORT_TO_PYPI.get(root, root)
|
|
48
|
+
|
|
49
|
+
def extract_import_names(nodes):
|
|
50
|
+
"Extract top-level module names from import nodes."
|
|
51
|
+
names = set()
|
|
52
|
+
for n in nodes:
|
|
53
|
+
if n.kind != Kind.IMP: continue
|
|
54
|
+
tree = ast.parse(n.src)
|
|
55
|
+
for stmt in ast.walk(tree):
|
|
56
|
+
if isinstance(stmt, ast.Import):
|
|
57
|
+
for alias in stmt.names:
|
|
58
|
+
names.add(alias.name.split('.')[0])
|
|
59
|
+
elif isinstance(stmt, ast.ImportFrom) and stmt.module:
|
|
60
|
+
names.add(stmt.module.split('.')[0])
|
|
61
|
+
return names
|
|
62
|
+
|
|
63
|
+
def pep723_header(deps):
|
|
64
|
+
"Generate PEP 723 inline script metadata."
|
|
65
|
+
deps_str = ', '.join(f'"{d}"' for d in sorted(deps))
|
|
66
|
+
return f'# /// script\n# dependencies = [{deps_str}]\n# ///\n'
|
|
67
|
+
|
|
68
|
+
def bundle(root='.', name=None):
|
|
69
|
+
"Bundle all notebooks into a single Python file with PEP 723 dependencies."
|
|
70
|
+
cfg = read_config(root)
|
|
71
|
+
meta, mods = scan(root)
|
|
72
|
+
|
|
73
|
+
# Collect all nodes
|
|
74
|
+
all_nodes = [n for _, nodes in mods for n in nodes]
|
|
75
|
+
|
|
76
|
+
# Extract dependencies
|
|
77
|
+
import_names = extract_import_names(all_nodes)
|
|
78
|
+
# Filter out stdlib and local modules
|
|
79
|
+
mod_names = [m for m, _ in mods]
|
|
80
|
+
external = {get_pypi_name(n) for n in import_names if n not in mod_names}
|
|
81
|
+
|
|
82
|
+
# Build output
|
|
83
|
+
header = pep723_header(external)
|
|
84
|
+
imports = '\n'.join(n.src for n in all_nodes if n.kind == Kind.IMP)
|
|
85
|
+
consts = '\n'.join(n.src for n in all_nodes if n.kind == Kind.CONST)
|
|
86
|
+
exports = '\n\n'.join(clean(n.src) for n in all_nodes if n.kind == Kind.EXP)
|
|
87
|
+
|
|
88
|
+
content = '\n\n'.join(p for p in [header, imports, consts, exports] if p.strip())
|
|
89
|
+
|
|
90
|
+
# Determine output path
|
|
91
|
+
if name:
|
|
92
|
+
out_path = Path(root) / name
|
|
93
|
+
else:
|
|
94
|
+
out_path = Path(root) / cfg.out / meta['name'] / '__init__.py'
|
|
95
|
+
|
|
96
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
out_path.write_text(content)
|
|
98
|
+
return f"Bundled to {out_path}"
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from .core import Kind, Param, Node, Config, read_config
|
|
2
|
+
from .read import scan, nb_name, read_meta
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import ast, re, os
|
|
5
|
+
import marimo as mo
|
|
6
|
+
from functools import partial
|
|
7
|
+
from fastcore.xml import Span, Code, Li, Article, Div, Ul, P, FT, to_xml, Pre, Link, A, Iframe, Button, H1, H2, H3, Nav, Aside, Header, Input, NotStr, Strong, Main
|
|
8
|
+
from fasthtml.components import ft, Html, Head, Script, Body, show, Style, Title
|
|
9
|
+
|
|
10
|
+
icons = {'home': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-house-icon lucide-house"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>', 'pypi': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-blocks-icon lucide-blocks"><path d="M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2"/><rect x="14" y="2" width="8" height="8" rx="1"/></svg>', 'menu': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu-icon lucide-menu"><path d="M4 5h16"/><path d="M4 12h16"/><path d="M4 19h16"/></svg>', 'x': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>', 'github': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github-icon lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>', 'code': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-icon lucide-code"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>', 'info': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info-icon lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>', 'calendar': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar1-icon lucide-calendar-1"><path d="M11 14h1v4"/><path d="M16 2v4"/><path d="M3 10h18"/><path d="M8 2v4"/><rect x="3" y="4" width="18" height="18" rx="2"/></svg>', 'circle-x': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>', 'external-link': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link-icon lucide-external-link"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>'}
|
|
11
|
+
|
|
12
|
+
def cls_sig(
|
|
13
|
+
n:Node, # the node to generate signature for
|
|
14
|
+
dataclass=False, # whether to include @dataclass decorator
|
|
15
|
+
)->str: # formatted class signature
|
|
16
|
+
"Generate a class signature string."
|
|
17
|
+
header = f"@dataclass\nclass {n.name}:" if dataclass else f"class {n.name}:"
|
|
18
|
+
lines = [header]
|
|
19
|
+
if n.doc: lines.append(f' """{n.doc}"""')
|
|
20
|
+
for p in n.params:
|
|
21
|
+
attr = f" {p.name}{f': {p.anno}' if p.anno else ''}{f' = {p.default}' if p.default else ''}"
|
|
22
|
+
if p.doc: attr += f" # {p.doc}"
|
|
23
|
+
lines.append(attr)
|
|
24
|
+
for m in n.methods:
|
|
25
|
+
ps = ', '.join(f"{p.name}{f': {p.anno}' if p.anno else ''}{f'={p.default}' if p.default else ''}" for p in m['params'])
|
|
26
|
+
ret = f" -> {m['ret'][0]}" if m['ret'] else ""
|
|
27
|
+
lines.append(f" def {m['name']}({ps}){ret}:")
|
|
28
|
+
if m['doc']: lines.append(f' """{m["doc"]}"""')
|
|
29
|
+
return '\n'.join(lines)
|
|
30
|
+
|
|
31
|
+
def fn_sig(
|
|
32
|
+
n: Node, # the node to generate signature for
|
|
33
|
+
is_async=False # async ?
|
|
34
|
+
)->str: # formatted signature string
|
|
35
|
+
"Generate a function signature string with inline parameter documentation."
|
|
36
|
+
prefix = 'async def' if is_async else 'def'
|
|
37
|
+
ret = f" -> {n.ret[0]}" if n.ret else ""
|
|
38
|
+
if not n.params:
|
|
39
|
+
sig = f"{prefix} {n.name}(){ret}:"
|
|
40
|
+
return f'{sig}\n """{n.doc}"""' if n.doc else sig
|
|
41
|
+
params = [f" {p.name}{f': {p.anno}' if p.anno else ''}{f'={p.default}' if p.default else ''},{f' # {p.doc}' if p.doc else ''}" for p in n.params]
|
|
42
|
+
params[-1] = params[-1].replace(',', '')
|
|
43
|
+
lines = [f"{prefix} {n.name}("] + params + [f"){ret}:"]
|
|
44
|
+
if n.doc: lines.append(f' """{n.doc}"""')
|
|
45
|
+
return '\n'.join(lines)
|
|
46
|
+
|
|
47
|
+
def sig(
|
|
48
|
+
n:Node, # the node to generate signature for
|
|
49
|
+
)->str: # formatted signature string
|
|
50
|
+
"Generate a signature string for a class or function node."
|
|
51
|
+
t = exp_type(n)
|
|
52
|
+
if t == 'class': return cls_sig(n, dataclass=n.src.lstrip().startswith('@dataclass'))
|
|
53
|
+
return fn_sig(n, is_async=t == 'async')
|
|
54
|
+
|
|
55
|
+
def write_llms(
|
|
56
|
+
meta: dict, # project metadata from pyproject.toml
|
|
57
|
+
nodes: list, # list of Node objects to document
|
|
58
|
+
root: str='.' # root directory containing pyproject.toml
|
|
59
|
+
):
|
|
60
|
+
"Write API signatures to llms.txt file for LLM consumption."
|
|
61
|
+
cfg = read_config(root)
|
|
62
|
+
sigs = '\n\n'.join(sig(n) for n in nodes if not n.name.startswith('__') and 'nodoc' not in n.hash_pipes)
|
|
63
|
+
content = f"# {meta['name']}\n\n> {meta['desc']}\n\nVersion: {meta['version']}\n\n## API\n\n```python\n{sigs}\n```"
|
|
64
|
+
Path(cfg.docs).mkdir(exist_ok=True)
|
|
65
|
+
(Path(cfg.docs)/'llms.txt').write_text(content)
|
|
66
|
+
|
|
67
|
+
def exp_type(n):
|
|
68
|
+
if n.methods or 'class ' in n.src: return 'class'
|
|
69
|
+
if n.src.lstrip().startswith('async def'): return 'async'
|
|
70
|
+
return 'func'
|
|
71
|
+
|
|
72
|
+
def render_param(p):
|
|
73
|
+
parts = [Code(p.name)]
|
|
74
|
+
if p.anno: parts.append(Span(f": {p.anno}", style="color: #666;"))
|
|
75
|
+
if p.default: parts.append(Span(f" = {p.default}", style="color: #888;"))
|
|
76
|
+
if p.doc: parts.append(Span(f" — {p.doc}", style="color: #555; font-style: italic;"))
|
|
77
|
+
return Li(*parts)
|
|
78
|
+
|
|
79
|
+
def nb_path(
|
|
80
|
+
mod_name,
|
|
81
|
+
root='.'
|
|
82
|
+
):
|
|
83
|
+
'''[TODO] '''
|
|
84
|
+
cfg = read_config(root)
|
|
85
|
+
for f in (Path(root) / cfg.nbs).glob('*.py'):
|
|
86
|
+
if nb_name(f, root) == mod_name: return f.relative_to(root)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def render_node(
|
|
90
|
+
n,
|
|
91
|
+
repo_url=None,
|
|
92
|
+
root='.'
|
|
93
|
+
):
|
|
94
|
+
'''Builds a `node` for docs'''
|
|
95
|
+
t = exp_type(n)
|
|
96
|
+
signature = sig(n)
|
|
97
|
+
lines = signature.split('\n')
|
|
98
|
+
line_nums = '\n'.join(str(i+1) for i in range(len(lines)))
|
|
99
|
+
node_id = f"code-{n.module}-{n.name}"
|
|
100
|
+
tag_colors = {'func': '#10b981', 'async': '#f59e0b', 'class': '#8b5cf6'}
|
|
101
|
+
tag = Span(t, style=f"padding: 0.25rem 0.6rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; background: {tag_colors.get(t, '#666')}; color: white;")
|
|
102
|
+
full_name = Span(Span(f"{n.module}.", style="color: #666;"), Span(n.name, style="color: #e5e5e5;"), style="font-weight: 500; font-size: 1rem; margin-left: 0.75rem;") if n.module else Span(n.name, style="font-weight: 500; font-size: 1rem; color: #e5e5e5; margin-left: 0.75rem;")
|
|
103
|
+
nb = nb_path(n.module, root)
|
|
104
|
+
btn_style = "display: flex; align-items: center; gap: 0.25rem; background: #333; border: 1px solid #444; color: #ccc; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem;"
|
|
105
|
+
link_style = "text-decoration: none;"
|
|
106
|
+
copy_btn = Button("📋", onclick=f"navigator.clipboard.writeText(document.getElementById('{node_id}').textContent).then(() => this.textContent = '✓').then(() => setTimeout(() => this.textContent = '📋', 1500))", style="background: transparent; border: none; cursor: pointer; font-size: 0.9rem; padding: 0.25rem;")
|
|
107
|
+
source_btn = A(Button(Icon('github', size=16), "Source", style=btn_style), href=f"{repo_url}/blob/master/{nb}#L{n.lineno}", target="_blank", style=link_style) if repo_url and nb and n.lineno else None
|
|
108
|
+
edit_btn = A(Button(Icon('code', size=16), "Edit", style=btn_style), href=f"{repo_url}/edit/master/{nb}", target="_blank", style=link_style) if repo_url and nb else None
|
|
109
|
+
blame_btn = A(Button(Icon('info', size=16), "Blame", style=btn_style), href=f"{repo_url}/blame/master/{nb}#L{n.lineno}", target="_blank", style=link_style) if repo_url and nb and n.lineno else None
|
|
110
|
+
history_btn = A(Button(Icon('calendar', size=16), "History", style=btn_style), href=f"{repo_url}/commits/master/{nb}", target="_blank", style=link_style) if repo_url and nb else None
|
|
111
|
+
issue_btn = A(Button(Icon('circle-x', size=16), "Issue", style=btn_style), href=f"{repo_url}/issues/new?title=Issue%20with%20{n.name}&body=Found%20in%20{nb}%23L{n.lineno}", target="_blank", style=link_style) if repo_url and nb else None
|
|
112
|
+
header = Div(
|
|
113
|
+
Div(tag, full_name, style="display: flex; align-items: center;"),
|
|
114
|
+
Div(copy_btn, source_btn, edit_btn, blame_btn, history_btn, issue_btn, style="display: flex; align-items: center; gap: 0.5rem;"),
|
|
115
|
+
style="display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem;")
|
|
116
|
+
doc_line = P(n.doc, style="margin: 0; padding: 0 1rem 0.5rem 1rem; color: #888; font-size: 0.85rem;") if n.doc else None
|
|
117
|
+
code_block = Div(
|
|
118
|
+
Pre(Code(line_nums), style="margin: 0; padding: 0; color: #555; text-align: right; user-select: none; font-size: 0.8rem; line-height: 1.6;"),
|
|
119
|
+
Pre(Code(signature, cls="language-python", id=node_id), style="margin: 0; padding: 0; flex: 1; overflow-x: auto; font-size: 0.8rem; line-height: 1.6;"),
|
|
120
|
+
style="display: flex; background: #1a1a1a; border-top: 1px solid #2a2a2a;")
|
|
121
|
+
return Article(header, doc_line, code_block, style="margin-bottom: 0.75rem; border-radius: 8px; overflow: hidden; background: #1e1e1e;")
|
|
122
|
+
|
|
123
|
+
def render_module_page(
|
|
124
|
+
mod_name,
|
|
125
|
+
mod_nodes,
|
|
126
|
+
all_mod_names,
|
|
127
|
+
meta,
|
|
128
|
+
root='.'):
|
|
129
|
+
'''Builds a Module Page'''
|
|
130
|
+
repo_url = meta.get('urls', {}).get('Repository')
|
|
131
|
+
exp_nodes = [n for n in mod_nodes if n.kind == Kind.EXP]
|
|
132
|
+
content = Div(*[render_node(n, repo_url, root) for n in exp_nodes], style="padding: 1rem; background: #121212; overflow-y: auto;")
|
|
133
|
+
head_elements = [
|
|
134
|
+
Script(type="module", src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.7/bundles/datastar.js"),
|
|
135
|
+
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css"),
|
|
136
|
+
Script(src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"),
|
|
137
|
+
Script("hljs.highlightAll();"),
|
|
138
|
+
Style("body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; } code { font-family: 'SF Mono', Consolas, monospace; }"),
|
|
139
|
+
Title(f"{mod_name} - {meta['name']}")]
|
|
140
|
+
search_input = Input(type="text", placeholder="Search...", style="width: 100%; padding: 0.5rem; border: 1px solid #333; border-radius: 4px; background: #252525; color: #fff; margin-bottom: 1rem;", **{"data-bind": "search"})
|
|
141
|
+
nav_links = [Li(A("← Index", href="index.html", style="color: #888; text-decoration: none; font-size: 0.85rem;"))] + [Li(A(m, href=f"{m}.html", style=f"color: {'#fff' if m == mod_name else '#aaa'}; text-decoration: none;")) for m in all_mod_names]
|
|
142
|
+
nav = Nav(
|
|
143
|
+
H3(meta['name'], style="margin: 0 0 1rem 0; color: #fff;"),
|
|
144
|
+
search_input,
|
|
145
|
+
Ul(*nav_links, style="list-style: none; padding: 0; margin: 0;"),
|
|
146
|
+
style="padding: 1rem; background: #1a1a1a; min-width: 180px;")
|
|
147
|
+
btn_style = "display: flex; align-items: center; gap: 0.25rem; background: #333; border: 1px solid #444; color: #ccc; padding: 0.5rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem;"
|
|
148
|
+
wasm_btn = A(Button(Icon('external-link', size=16), "Run in Browser", style=btn_style), href=f"wasm/{mod_name}/index.html", target="_blank", style="text-decoration: none;")
|
|
149
|
+
header = Header(
|
|
150
|
+
Div(H1(mod_name, style="margin: 0; font-size: 1.5rem; color: #fff;"), wasm_btn, style="display: flex; align-items: center; justify-content: space-between;"),
|
|
151
|
+
style="padding: 1rem; background: #1e1e1e; border-bottom: 1px solid #333;")
|
|
152
|
+
body = Body(nav, Div(header, content, style="flex: 1; display: flex; flex-direction: column;"), style="display: flex; height: 100vh; margin: 0; background: #121212;", **{"data-signals": "{search: ''}"})
|
|
153
|
+
return Html(Head(*head_elements), body)
|
|
154
|
+
|
|
155
|
+
def build_docs(
|
|
156
|
+
root='.' # the project root (this should never really change)
|
|
157
|
+
):
|
|
158
|
+
'''Builds the static documentation website'''
|
|
159
|
+
cfg = read_config(root)
|
|
160
|
+
meta = read_meta(root)
|
|
161
|
+
_, mods = scan(root)
|
|
162
|
+
mod_names = [name for name, _ in mods]
|
|
163
|
+
docs_path = Path(root) / cfg.docs
|
|
164
|
+
docs_path.mkdir(exist_ok=True)
|
|
165
|
+
(docs_path / "index.html").write_text(to_xml(render_index_page(meta, mods)))
|
|
166
|
+
for mod_name, mod_nodes in mods:
|
|
167
|
+
(docs_path / f"{mod_name}.html").write_text(to_xml(render_module_page(mod_name, mod_nodes, mod_names, meta, root)))
|
|
168
|
+
return f"Generated index + {len(mods)} module pages in {docs_path}"
|
|
169
|
+
|
|
170
|
+
cfg = read_config(root)
|
|
171
|
+
|
|
172
|
+
nbs_dir = Path(root) / cfg.nbs
|
|
173
|
+
|
|
174
|
+
wasm_dir = Path(root) / cfg.docs / 'wasm'
|
|
175
|
+
|
|
176
|
+
wasm_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
|
|
178
|
+
for f in nbs_dir.glob('*.py'):
|
|
179
|
+
name = nb_name(f, root)
|
|
180
|
+
if name:
|
|
181
|
+
os.system(f'marimo export html-wasm {f} -o {wasm_dir}/{name} --mode edit')
|
|
182
|
+
|
|
183
|
+
def write_nojekyll(root='.'):
|
|
184
|
+
cfg = read_config(root)
|
|
185
|
+
Path(root, cfg.docs, '.nojekyll').touch()
|
|
186
|
+
|
|
187
|
+
def html_preview(width='100%', height='300px'):
|
|
188
|
+
"Display FT components in an IFrame"
|
|
189
|
+
def _preview(*components): show(Iframe(srcdoc=to_xml(components[0] if len(components) == 1 else Div(*components)), width=width, height=height))
|
|
190
|
+
return _preview
|
|
191
|
+
|
|
192
|
+
def render_index_page(meta, mods, repo_url=None):
|
|
193
|
+
mod_names = [name for name, _ in mods]
|
|
194
|
+
head_elements = [
|
|
195
|
+
Script(type="module", src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.7/bundles/datastar.js"),
|
|
196
|
+
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css"),
|
|
197
|
+
Script(src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"),
|
|
198
|
+
Script("hljs.highlightAll();"),
|
|
199
|
+
Style("body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; } code { font-family: 'SF Mono', Consolas, monospace; }"),
|
|
200
|
+
Title(meta['name'])]
|
|
201
|
+
nav_links = [Li(A(m, href=f"{m}.html", style="color: #aaa; text-decoration: none;")) for m in mod_names]
|
|
202
|
+
nav = Nav(
|
|
203
|
+
H3(meta['name'], style="margin: 0 0 1rem 0; color: #fff;"),
|
|
204
|
+
Ul(*nav_links, style="list-style: none; padding: 0; margin: 0;"),
|
|
205
|
+
style="padding: 1rem; background: #1a1a1a; min-width: 180px;")
|
|
206
|
+
module_cards = [A(Div(H3(name, style="margin: 0 0 0.5rem 0; color: #fff;"), P(f"{len([n for n in nodes if n.kind == Kind.EXP])} exports", style="margin: 0; color: #888;"),
|
|
207
|
+
style="padding: 1rem; background: #1e1e1e; border-radius: 8px;"), href=f"{name}.html", style="text-decoration: none;") for name, nodes in mods]
|
|
208
|
+
content = Div(
|
|
209
|
+
H1(meta['name'], style="margin: 0 0 0.5rem 0; color: #fff;"),
|
|
210
|
+
P(meta['desc'], style="color: #888; margin: 0 0 2rem 0;"),
|
|
211
|
+
P(f"Version {meta['version']}", style="color: #666; margin: 0 0 2rem 0; font-size: 0.9rem;"),
|
|
212
|
+
H2("Modules", style="color: #fff; margin: 0 0 1rem 0; font-size: 1.2rem;"),
|
|
213
|
+
Div(*module_cards, style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem;"),
|
|
214
|
+
style="padding: 2rem; flex: 1;")
|
|
215
|
+
body = Body(nav, content, style="display: flex; min-height: 100vh; margin: 0; background: #121212;")
|
|
216
|
+
return Html(Head(*head_elements), body)
|
|
217
|
+
|
|
218
|
+
def Icon(name: str, # name of the icon MUST be in icon_dict
|
|
219
|
+
size=24, # value to be passed to height and width of the icon
|
|
220
|
+
stroke=1.5, # stroke width
|
|
221
|
+
cls=None, # css class
|
|
222
|
+
icon_dict:dict=icons, # Dict of icons {"name":"<svg...>"}
|
|
223
|
+
**kwargs # passed to through to FT
|
|
224
|
+
) -> 'Any': # Follow recomendation from fastHTML docs
|
|
225
|
+
'''
|
|
226
|
+
Creates a custom html compliant <icon-{name}>...
|
|
227
|
+
Intended to be used with a Global Dict of icons {"home": "<svg...", "info": "<svg..."}
|
|
228
|
+
Icon('home') -> <icon-home> .... </icon-home>
|
|
229
|
+
'''
|
|
230
|
+
if name not in icon_dict: raise ValueError(f"Icon '{name}' not found")
|
|
231
|
+
|
|
232
|
+
# count=1 Replace only the first occurrence of width & height 99% of time this is what you want
|
|
233
|
+
svg_string = icon_dict[name]
|
|
234
|
+
svg_string = re.sub(r'width="\d+"', f'width="{size}"', svg_string, count=1)
|
|
235
|
+
svg_string = re.sub(r'height="\d+"', f'height="{size}"', svg_string, count=1)
|
|
236
|
+
svg_string = re.sub(r'stroke-width="\d+"', f'stroke-width="{stroke}"', svg_string)
|
|
237
|
+
|
|
238
|
+
return ft(f'icon-{name}', NotStr(svg_string), cls=cls, **kwargs)
|
|
@@ -92,15 +92,15 @@ def parse_const(
|
|
|
92
92
|
n:ast.AST, # AST node to check
|
|
93
93
|
ls:list, # source lines (unused)
|
|
94
94
|
)->Node|None: # Node if constant assignment, else None
|
|
95
|
-
"Extract constant definition
|
|
95
|
+
"Extract constant definition from assignment."
|
|
96
96
|
if not isinstance(n, ast.Assign): return None
|
|
97
97
|
for t in n.targets:
|
|
98
|
-
if isinstance(t, ast.Name)
|
|
98
|
+
if isinstance(t, ast.Name): return Node(Kind.CONST, t.id, ast.unparse(n))
|
|
99
99
|
|
|
100
100
|
def parse_export(
|
|
101
101
|
n:ast.AST, # AST node to check
|
|
102
102
|
ls:list, # source lines for inline doc and decorators
|
|
103
|
-
cfg:Config
|
|
103
|
+
cfg:Config # configuration object
|
|
104
104
|
)->Node|None: # Node if exported function/class, else None
|
|
105
105
|
"Extract exported function or class decorated with export decorators from config."
|
|
106
106
|
if not isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): return None
|
|
@@ -108,8 +108,8 @@ def parse_export(
|
|
|
108
108
|
if not export_dec or n.name.startswith('test_'): return None
|
|
109
109
|
doc,src = ast.get_docstring(n) or '', src_with_decs(n, ls)
|
|
110
110
|
hash_pipes = parse_hash_pipe(ls, export_dec)
|
|
111
|
-
if isinstance(n, ast.ClassDef): return Node(Kind.EXP, n.name, src, doc, parse_class_params(n, ls), parse_class_methods(n, ls), None, hash_pipes)
|
|
112
|
-
return Node(Kind.EXP, n.name, src, doc, parse_params(n, ls), [], parse_ret(n, ls), hash_pipes)
|
|
111
|
+
if isinstance(n, ast.ClassDef): return Node(Kind.EXP, n.name, src, doc, parse_class_params(n, ls), parse_class_methods(n, ls), None, hash_pipes, '', n.lineno)
|
|
112
|
+
return Node(Kind.EXP, n.name, src, doc, parse_params(n, ls), [], parse_ret(n, ls), hash_pipes, '', n.lineno)
|
|
113
113
|
|
|
114
114
|
def parse_node(
|
|
115
115
|
n:ast.AST, # AST node to parse
|
|
@@ -118,10 +118,22 @@ def parse_node(
|
|
|
118
118
|
): # yields Node objects for imports, constants, and exports
|
|
119
119
|
"Extract importable nodes from an AST node."
|
|
120
120
|
ls = src.splitlines()
|
|
121
|
+
|
|
122
|
+
# Handle setup cells
|
|
121
123
|
if isinstance(n, ast.With):
|
|
122
124
|
for s in n.body:
|
|
123
125
|
if (node := parse_import(s, ls)): yield node
|
|
124
126
|
if (node := parse_const(s, ls)): yield node
|
|
127
|
+
|
|
128
|
+
# Handle export-named cells (e.g. def export(): or def export_main():)
|
|
129
|
+
if isinstance(n, ast.FunctionDef) and n.name.startswith('export'):
|
|
130
|
+
body = [s for s in n.body if not isinstance(s, ast.Return)]
|
|
131
|
+
if body:
|
|
132
|
+
src = '\n\n'.join(ast.unparse(s) for s in body)
|
|
133
|
+
yield Node(Kind.EXP, n.name, src)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# Handle decorated exports
|
|
125
137
|
if (node := parse_export(n, ls, cfg)): yield node
|
|
126
138
|
|
|
127
139
|
def parse_file(
|
|
@@ -138,13 +150,27 @@ def parse_file(
|
|
|
138
150
|
|
|
139
151
|
def read_meta(
|
|
140
152
|
root='.', # project root directory containing pyproject.toml
|
|
141
|
-
)->dict: # metadata dict with name, version, desc, license, author
|
|
153
|
+
)->dict: # metadata dict with name, version, desc, license, author, urls
|
|
142
154
|
"Read project metadata from pyproject.toml."
|
|
143
|
-
with open(Path(root)/'pyproject.toml', 'rb') as f:
|
|
155
|
+
with open(Path(root)/'pyproject.toml', 'rb') as f:
|
|
156
|
+
p = tomllib.load(f).get('project', {})
|
|
157
|
+
|
|
158
|
+
# Extract author
|
|
144
159
|
a = (p.get('authors') or [{}])[0]
|
|
145
160
|
author = f"{a.get('name','')} <{a.get('email','')}>".strip(' <>') if isinstance(a, dict) else str(a)
|
|
161
|
+
|
|
162
|
+
# Extract license
|
|
146
163
|
lic = p.get('license', {})
|
|
147
|
-
|
|
164
|
+
license_text = lic.get('text','') if isinstance(lic, dict) else lic
|
|
165
|
+
|
|
166
|
+
return dict(
|
|
167
|
+
name=p.get('name',''),
|
|
168
|
+
version=p.get('version','0.0.0'),
|
|
169
|
+
desc=p.get('description',''),
|
|
170
|
+
license=license_text,
|
|
171
|
+
author=author,
|
|
172
|
+
urls=p.get('urls', {})
|
|
173
|
+
)
|
|
148
174
|
|
|
149
175
|
def nb_name(
|
|
150
176
|
f: Path, # file path to extract notebook name from
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
from .core import Kind, Param, Node, Config, read_config
|
|
2
|
-
from .read import scan, read_meta
|
|
3
|
-
from .pkg import write_mod, write_init
|
|
4
|
-
from .docs import write_llms
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
import ast, shutil, re
|
|
7
|
-
|
|
8
|
-
def build(
|
|
9
|
-
root='.', # root directory containing pyproject.toml
|
|
10
|
-
)->str: # path to built package
|
|
11
|
-
"Build a Python package from notebooks."
|
|
12
|
-
cfg = read_config(root)
|
|
13
|
-
meta, mods = scan(root)
|
|
14
|
-
mod_names = [name for name, _ in mods]
|
|
15
|
-
pkg = Path(root) / cfg.out / meta['name'].replace('-', '_')
|
|
16
|
-
if pkg.exists(): shutil.rmtree(pkg)
|
|
17
|
-
pkg.mkdir(parents=True, exist_ok=True)
|
|
18
|
-
for name, nodes in mods:
|
|
19
|
-
stripped = re.sub(r'^[a-z]_', '', name)
|
|
20
|
-
if stripped != 'index' and any(n.kind == Kind.EXP for n in nodes): write_mod(pkg/f'{stripped}.py', nodes, mod_names)
|
|
21
|
-
write_init(pkg/'__init__.py', meta, mods)
|
|
22
|
-
all_exp = [n for _, nodes in mods for n in nodes if n.kind == Kind.EXP]
|
|
23
|
-
if all_exp: write_llms(meta, all_exp)
|
|
24
|
-
return str(pkg)
|
|
25
|
-
|
|
26
|
-
def tidy():
|
|
27
|
-
"Remove cache and temporary files (__pycache__, __marimo__, .pytest_cache, etc)."
|
|
28
|
-
import shutil
|
|
29
|
-
for p in Path('.').rglob('__pycache__'): shutil.rmtree(p, ignore_errors=True)
|
|
30
|
-
for p in Path('.').rglob('__marimo__'): shutil.rmtree(p, ignore_errors=True)
|
|
31
|
-
for p in Path('.').rglob('.pytest_cache'): shutil.rmtree(p, ignore_errors=True)
|
|
32
|
-
for p in Path('.').rglob('*.pyc'): p.unlink(missing_ok=True)
|
|
33
|
-
print("Cleaned cache files")
|
|
34
|
-
|
|
35
|
-
def nuke():
|
|
36
|
-
"Remove all build artifacts (dist, docs, src) and cache files."
|
|
37
|
-
import shutil
|
|
38
|
-
tidy()
|
|
39
|
-
for d in ['dist', 'docs', 'src', 'temp']: shutil.rmtree(d, ignore_errors=True)
|
|
40
|
-
print("Nuked build artifacts")
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from .build import build, tidy, nuke
|
|
2
|
-
from .publish import publish
|
|
3
|
-
import sys, subprocess
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
def main():
|
|
7
|
-
if len(sys.argv) < 2: print("Usage: md [build|publish|tidy|nuke]"); sys.exit(1)
|
|
8
|
-
cmd = sys.argv[1]
|
|
9
|
-
if cmd == 'build':
|
|
10
|
-
print(f"Built package at: {build()}")
|
|
11
|
-
elif cmd == 'publish':
|
|
12
|
-
test = '--test' in sys.argv or '-t' in sys.argv
|
|
13
|
-
target = "TestPyPI" if test else "PyPI"
|
|
14
|
-
if input(f"Publish to {target}? [y/N] ").lower() != 'y': print("Aborted"); sys.exit(0)
|
|
15
|
-
publish(test=test)
|
|
16
|
-
elif cmd == 'tidy': tidy()
|
|
17
|
-
elif cmd == 'nuke': nuke()
|
|
18
|
-
else: print(f"Unknown command: {cmd}"); sys.exit(1)
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from .core import Kind, Param, Node, Config, read_config
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
import ast
|
|
4
|
-
import marimo as mo
|
|
5
|
-
from functools import partial
|
|
6
|
-
|
|
7
|
-
def cls_sig(
|
|
8
|
-
n:Node, # the node to generate signature for
|
|
9
|
-
dataclass=False, # whether to include @dataclass decorator
|
|
10
|
-
)->str: # formatted class signature
|
|
11
|
-
"Generate a class signature string."
|
|
12
|
-
header = f"@dataclass\nclass {n.name}:" if dataclass else f"class {n.name}:"
|
|
13
|
-
lines = [header]
|
|
14
|
-
if n.doc: lines.append(f' """{n.doc}"""')
|
|
15
|
-
for p in n.params:
|
|
16
|
-
attr = f" {p.name}{f': {p.anno}' if p.anno else ''}{f' = {p.default}' if p.default else ''}"
|
|
17
|
-
if p.doc: attr += f" # {p.doc}"
|
|
18
|
-
lines.append(attr)
|
|
19
|
-
for m in n.methods:
|
|
20
|
-
ps = ', '.join(f"{p.name}{f': {p.anno}' if p.anno else ''}{f'={p.default}' if p.default else ''}" for p in m['params'])
|
|
21
|
-
ret = f" -> {m['ret'][0]}" if m['ret'] else ""
|
|
22
|
-
lines.append(f" def {m['name']}({ps}){ret}:")
|
|
23
|
-
if m['doc']: lines.append(f' """{m["doc"]}"""')
|
|
24
|
-
return '\n'.join(lines)
|
|
25
|
-
|
|
26
|
-
def fn_sig(n, is_async=False):
|
|
27
|
-
"Generate a function signature string with inline parameter documentation."
|
|
28
|
-
prefix = 'async def' if is_async else 'def'
|
|
29
|
-
ret = f" -> {n.ret[0]}" if n.ret else ""
|
|
30
|
-
if not n.params:
|
|
31
|
-
sig = f"{prefix} {n.name}(){ret}:"
|
|
32
|
-
return f'{sig}\n """{n.doc}"""' if n.doc else sig
|
|
33
|
-
params = [f" {p.name}{f': {p.anno}' if p.anno else ''}{f'={p.default}' if p.default else ''},{f' # {p.doc}' if p.doc else ''}" for p in n.params]
|
|
34
|
-
params[-1] = params[-1].replace(',', '')
|
|
35
|
-
lines = [f"{prefix} {n.name}("] + params + [f"){ret}:"]
|
|
36
|
-
if n.doc: lines.append(f' """{n.doc}"""')
|
|
37
|
-
return '\n'.join(lines)
|
|
38
|
-
|
|
39
|
-
def sig(
|
|
40
|
-
n:Node, # the node to generate signature for
|
|
41
|
-
)->str: # formatted signature string
|
|
42
|
-
"Generate a signature string for a class or function node."
|
|
43
|
-
src = n.src.lstrip()
|
|
44
|
-
if n.methods or src.startswith('class ') or src.startswith('@dataclass'):
|
|
45
|
-
return cls_sig(n, dataclass=src.startswith('@dataclass'))
|
|
46
|
-
return fn_sig(n, is_async=src.startswith('async def'))
|
|
47
|
-
|
|
48
|
-
def write_llms(
|
|
49
|
-
meta: dict, # project metadata from pyproject.toml
|
|
50
|
-
nodes: list, # list of Node objects to document
|
|
51
|
-
root: str='.' # root directory containing pyproject.toml
|
|
52
|
-
):
|
|
53
|
-
"Write API signatures to llms.txt file for LLM consumption."
|
|
54
|
-
cfg = read_config(root)
|
|
55
|
-
sigs = '\n\n'.join(sig(n) for n in nodes if not n.name.startswith('__') and 'nodoc' not in n.hash_pipes)
|
|
56
|
-
content = f"# {meta['name']}\n\n> {meta['desc']}\n\nVersion: {meta['version']}\n\n## API\n\n```python\n{sigs}\n```"
|
|
57
|
-
Path(cfg.docs).mkdir(exist_ok=True)
|
|
58
|
-
(Path(cfg.docs)/'llms.txt').write_text(content)
|
|
File without changes
|
|
File without changes
|