neuromatch-course-kit 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.
- neuromatch_course_kit-0.1.1.dist-info/METADATA +28 -0
- neuromatch_course_kit-0.1.1.dist-info/RECORD +15 -0
- neuromatch_course_kit-0.1.1.dist-info/WHEEL +4 -0
- neuromatch_course_kit-0.1.1.dist-info/entry_points.txt +2 -0
- nma_kit/__init__.py +3 -0
- nma_kit/cells.py +28 -0
- nma_kit/cli.py +138 -0
- nma_kit/config.py +100 -0
- nma_kit/convert.py +442 -0
- nma_kit/feedback.py +254 -0
- nma_kit/media.py +243 -0
- nma_kit/pin.py +16 -0
- nma_kit/publish.py +67 -0
- nma_kit/scrolly.py +490 -0
- nma_kit/solutions.py +423 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: neuromatch-course-kit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Neuromatch course kit: notebook generation, content checks and site preparation for MyST courses
|
|
5
|
+
Project-URL: Homepage, https://github.com/neuromatch/course-kit
|
|
6
|
+
Project-URL: Issues, https://github.com/neuromatch/course-kit/issues
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: ipykernel
|
|
10
|
+
Requires-Dist: jupytext>=1.16
|
|
11
|
+
Requires-Dist: mystmd==1.11.0
|
|
12
|
+
Requires-Dist: nbclient
|
|
13
|
+
Requires-Dist: nbformat
|
|
14
|
+
Requires-Dist: pyyaml
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: ipywidgets; extra == 'test'
|
|
17
|
+
Requires-Dist: matplotlib; extra == 'test'
|
|
18
|
+
Requires-Dist: numpy; extra == 'test'
|
|
19
|
+
Requires-Dist: playwright; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest-rerunfailures; extra == 'test'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# neuromatch-course-kit
|
|
25
|
+
|
|
26
|
+
The `nma-kit` command line tool for Neuromatch MyST courses: notebook
|
|
27
|
+
generation, content checks and site preparation. See the
|
|
28
|
+
[course kit repository](https://github.com/neuromatch/course-kit) for docs.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
nma_kit/__init__.py,sha256=5a4Jy2tMbpaFNiZHNTjEexm_fHtPEgQVUuD8SDIqvRQ,128
|
|
2
|
+
nma_kit/cells.py,sha256=-KbtcZOEzZSStJoz1gBUVt4wUk172b8HbJAlehf7BoU,962
|
|
3
|
+
nma_kit/cli.py,sha256=v6mz_a9UA9V8xpN7i6xVr2_If9A5bxxGKlcMzrjr2N4,4898
|
|
4
|
+
nma_kit/config.py,sha256=qyPOWFe9jlQPJTrUCWBqe7zkF5i4IrczHf3LKVtWX94,3284
|
|
5
|
+
nma_kit/convert.py,sha256=ZJ4jeT8lkG0FKkzfwsCrINgQN5pGaUMbG6ABDVvVZxc,17872
|
|
6
|
+
nma_kit/feedback.py,sha256=Mdc0-NjX5upBjmTfRF_vRofrVBHSMUUeBGKuSjLKhZM,8861
|
|
7
|
+
nma_kit/media.py,sha256=48iANcTXztZD-amd-ENDzrigr9Zv__VLkG6qejKW2Rc,8911
|
|
8
|
+
nma_kit/pin.py,sha256=7NFUF5P0FksiO5Z1xv6-Zce8q-f1qraV1_awDlbpryQ,715
|
|
9
|
+
nma_kit/publish.py,sha256=aydvCvnAysjtJp72eUhHnOx6Sq6BwkZ2-TZFncdq7lo,2651
|
|
10
|
+
nma_kit/scrolly.py,sha256=nFmQMBpzm_K1sceNgRJi9TQsajID7M50EqLHUKfHURs,18385
|
|
11
|
+
nma_kit/solutions.py,sha256=X4YCNkxm-iMnPfn3jkccqkBq6MHeHpmxY9uKtpU-bo8,16422
|
|
12
|
+
neuromatch_course_kit-0.1.1.dist-info/METADATA,sha256=SS5WzRBB8UBXgJWEnZ6tW5uI9UxcZ4_RsXyFo4fwYxc,1065
|
|
13
|
+
neuromatch_course_kit-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
14
|
+
neuromatch_course_kit-0.1.1.dist-info/entry_points.txt,sha256=xlXXMTh2H8cUiIUPt59sm32FGGwJBx6qiAx8LdGSFpY,45
|
|
15
|
+
neuromatch_course_kit-0.1.1.dist-info/RECORD,,
|
nma_kit/__init__.py
ADDED
nma_kit/cells.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Editable cells: the `runtime:` frontmatter key (Python side).
|
|
2
|
+
|
|
3
|
+
`runtime_of` has a JavaScript twin, `runtimeOf` in js/src/cells/src/model.mjs.
|
|
4
|
+
Both are tested against tests/fixtures/cells/runtime.json.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from nma_kit import scrolly as nma_scrolly
|
|
10
|
+
|
|
11
|
+
RUNTIMES = ("browser", "colab")
|
|
12
|
+
RUNTIME_RE = re.compile(r"""^runtime:\s*(['"]?)([A-Za-z]+)\1\s*(#.*)?$""", re.M)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def runtime_of(text: str) -> str:
|
|
16
|
+
"""'browser' (default) or 'colab' (or the raw invalid value, for validate())."""
|
|
17
|
+
m = RUNTIME_RE.search(nma_scrolly.frontmatter_text(text))
|
|
18
|
+
return m.group(2) if m else "browser"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate(text: str) -> list[str]:
|
|
22
|
+
errors = []
|
|
23
|
+
runtime = runtime_of(text)
|
|
24
|
+
if runtime not in RUNTIMES:
|
|
25
|
+
errors.append(f"runtime: {runtime!r} must be one of {', '.join(RUNTIMES)}")
|
|
26
|
+
if runtime == "colab" and nma_scrolly.is_scrolly(text):
|
|
27
|
+
errors.append("runtime: colab can't be combined with layout: scrolly")
|
|
28
|
+
return errors
|
nma_kit/cli.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""`nma-kit`: the course kit's command line. Run it from the course root.
|
|
2
|
+
|
|
3
|
+
nma-kit check validate content and myst.yml
|
|
4
|
+
nma-kit notebooks [--dry-run] generate student/instructor notebooks and solution files
|
|
5
|
+
nma-kit render-scrolly pre-render scrolly page outputs into _build/scrolly/
|
|
6
|
+
nma-kit prepare-site add badges + fallback cells to the .md pages and pin the
|
|
7
|
+
kit URL in myst.yml (CI only, never commit the result)
|
|
8
|
+
nma-kit publish-notebooks push notebooks/ to the notebooks branch
|
|
9
|
+
|
|
10
|
+
Exit codes: 0 ok, 1 content problems, 2 usage or configuration errors.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from nma_kit import __version__
|
|
19
|
+
from nma_kit import convert, scrolly
|
|
20
|
+
from nma_kit.config import MYST_CONFIG, MissingConfig, read_settings
|
|
21
|
+
from nma_kit.pin import pin_kit_url
|
|
22
|
+
from nma_kit.publish import PublishError, publish_notebooks
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UsageError(Exception):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate(check_injected: bool) -> bool:
|
|
30
|
+
errors = convert.validate_content(check_injected=check_injected)
|
|
31
|
+
if errors:
|
|
32
|
+
print("Content rule violations:", file=sys.stderr)
|
|
33
|
+
for error in errors:
|
|
34
|
+
print(f" - {error}", file=sys.stderr)
|
|
35
|
+
return not errors
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _settings():
|
|
39
|
+
settings = read_settings()
|
|
40
|
+
try:
|
|
41
|
+
convert.require_github_repo(settings)
|
|
42
|
+
except ValueError as exc:
|
|
43
|
+
raise UsageError(str(exc)) from exc
|
|
44
|
+
return settings
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_check(args) -> int:
|
|
48
|
+
read_settings()
|
|
49
|
+
if not _validate(check_injected=True):
|
|
50
|
+
return 1
|
|
51
|
+
print("No content problems found.")
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_notebooks(args) -> int:
|
|
56
|
+
settings = _settings()
|
|
57
|
+
if not _validate(check_injected=True):
|
|
58
|
+
return 1
|
|
59
|
+
count = convert.process_tutorials(settings, args.dry_run, prepare_site=False)
|
|
60
|
+
print(f"Done. Processed {count} file(s).")
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cmd_render_scrolly(args) -> int:
|
|
65
|
+
return scrolly.render_all(read_settings().tutorials_dir)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_prepare_site(args) -> int:
|
|
69
|
+
settings = _settings()
|
|
70
|
+
if not _validate(check_injected=False):
|
|
71
|
+
return 1
|
|
72
|
+
count = convert.process_tutorials(settings, args.dry_run, prepare_site=True)
|
|
73
|
+
if not args.dry_run:
|
|
74
|
+
text = MYST_CONFIG.read_text(encoding="utf-8")
|
|
75
|
+
pinned = pin_kit_url(text, args.kit_version)
|
|
76
|
+
if pinned != text:
|
|
77
|
+
MYST_CONFIG.write_text(pinned, encoding="utf-8")
|
|
78
|
+
print(f"Pinned course kit URLs in {MYST_CONFIG} to {args.kit_version}")
|
|
79
|
+
print(f"Done. Prepared {count} page(s).")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_publish_notebooks(args) -> int:
|
|
84
|
+
settings = read_settings()
|
|
85
|
+
src = Path("notebooks")
|
|
86
|
+
if not src.is_dir():
|
|
87
|
+
raise UsageError("notebooks/ not found: run `nma-kit notebooks` first")
|
|
88
|
+
message = args.message or "chore: regenerate notebooks" + (
|
|
89
|
+
f" from {os.environ['GITHUB_SHA'][:12]}" if os.environ.get("GITHUB_SHA") else "")
|
|
90
|
+
pushed = publish_notebooks(src, settings.notebooks_branch, args.remote, message)
|
|
91
|
+
print(f"Pushed notebooks to {settings.notebooks_branch}." if pushed else "No notebook changes to push.")
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
96
|
+
parser = argparse.ArgumentParser(prog="nma-kit", description="Neuromatch course kit")
|
|
97
|
+
parser.add_argument("--version", action="version", version=f"nma-kit {__version__}")
|
|
98
|
+
sub = parser.add_subparsers(dest="command")
|
|
99
|
+
|
|
100
|
+
sub.add_parser("check", help="validate content and myst.yml").set_defaults(func=cmd_check)
|
|
101
|
+
|
|
102
|
+
p = sub.add_parser("notebooks", help="generate notebooks and solution files")
|
|
103
|
+
p.add_argument("--dry-run", action="store_true")
|
|
104
|
+
p.set_defaults(func=cmd_notebooks)
|
|
105
|
+
|
|
106
|
+
sub.add_parser("render-scrolly", help="pre-render scrolly pages").set_defaults(func=cmd_render_scrolly)
|
|
107
|
+
|
|
108
|
+
p = sub.add_parser("prepare-site", help="prepare the .md pages for `myst build` (CI only)")
|
|
109
|
+
p.add_argument("--dry-run", action="store_true")
|
|
110
|
+
p.add_argument("--kit-version", default=__version__,
|
|
111
|
+
help="version to pin course-kit URLs in myst.yml to (default: this CLI's)")
|
|
112
|
+
p.set_defaults(func=cmd_prepare_site)
|
|
113
|
+
|
|
114
|
+
p = sub.add_parser("publish-notebooks", help="push notebooks/ to the notebooks branch")
|
|
115
|
+
p.add_argument("--remote", default="origin")
|
|
116
|
+
p.add_argument("--message", default=None)
|
|
117
|
+
p.set_defaults(func=cmd_publish_notebooks)
|
|
118
|
+
return parser
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main(argv: list[str] | None = None) -> int:
|
|
122
|
+
parser = build_parser()
|
|
123
|
+
args = parser.parse_args(argv)
|
|
124
|
+
if not getattr(args, "func", None):
|
|
125
|
+
parser.print_usage(sys.stderr)
|
|
126
|
+
return 2
|
|
127
|
+
try:
|
|
128
|
+
return args.func(args)
|
|
129
|
+
except (MissingConfig, UsageError) as exc:
|
|
130
|
+
print(f"nma-kit: {exc}", file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
except PublishError as exc:
|
|
133
|
+
print(f"nma-kit: {exc}", file=sys.stderr)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
sys.exit(main())
|
nma_kit/config.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Course settings, read from the course's myst.yml.
|
|
3
|
+
|
|
4
|
+
Kit settings live in the top-level `nma:` block, as `section: {key: value}`
|
|
5
|
+
with string values. js/src/shared/config.mjs reads the same block with a
|
|
6
|
+
YAML parser too; both sides are tested against tests/fixtures/config/settings.json.
|
|
7
|
+
|
|
8
|
+
New settings (all optional, defaults are the template's conventions):
|
|
9
|
+
|
|
10
|
+
nma:
|
|
11
|
+
paths:
|
|
12
|
+
tutorials: tutorials # where the course's .md pages live
|
|
13
|
+
notebooks:
|
|
14
|
+
branch: notebooks-branch # where generated notebooks are published
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path, PurePosixPath
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
DEFAULT_TUTORIALS_DIR = "tutorials"
|
|
24
|
+
DEFAULT_NOTEBOOKS_BRANCH = "notebooks-branch"
|
|
25
|
+
NOTEBOOKS_DIR = "notebooks"
|
|
26
|
+
MYST_CONFIG = Path("myst.yml")
|
|
27
|
+
|
|
28
|
+
GITHUB_RE = re.compile(r"github\.com/([^\s'\"]+?)/?$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MissingConfig(Exception):
|
|
32
|
+
"""The course's myst.yml does not exist."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, path: Path):
|
|
35
|
+
super().__init__(f"{path} not found: run nma-kit from the course root (the folder with myst.yml)")
|
|
36
|
+
self.path = path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Settings:
|
|
41
|
+
github_repo: str | None
|
|
42
|
+
tutorials_dir: str = DEFAULT_TUTORIALS_DIR
|
|
43
|
+
notebooks_branch: str = DEFAULT_NOTEBOOKS_BRANCH
|
|
44
|
+
nma: dict = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _yaml(text: str) -> dict:
|
|
48
|
+
try:
|
|
49
|
+
data = yaml.safe_load(text)
|
|
50
|
+
except yaml.YAMLError:
|
|
51
|
+
return {}
|
|
52
|
+
return data if isinstance(data, dict) else {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_nma_block(text: str) -> dict:
|
|
56
|
+
"""The top-level `nma:` block of myst.yml ({} if absent or not a mapping)."""
|
|
57
|
+
block = _yaml(text).get("nma")
|
|
58
|
+
return block if isinstance(block, dict) else {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _github_repo(data: dict) -> str | None:
|
|
62
|
+
project = data.get("project")
|
|
63
|
+
url = project.get("github") if isinstance(project, dict) else None
|
|
64
|
+
match = GITHUB_RE.search(url.strip()) if isinstance(url, str) else None
|
|
65
|
+
return match.group(1) if match else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _setting(nma: dict, section: str, key: str, default: str) -> str:
|
|
69
|
+
value = (nma.get(section) or {}).get(key) if isinstance(nma.get(section), dict) else None
|
|
70
|
+
return value if isinstance(value, str) and value.strip() else default
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_settings(myst_text: str) -> Settings:
|
|
74
|
+
data = _yaml(myst_text)
|
|
75
|
+
nma = read_nma_block(myst_text)
|
|
76
|
+
return Settings(
|
|
77
|
+
github_repo=_github_repo(data),
|
|
78
|
+
tutorials_dir=_setting(nma, "paths", "tutorials", DEFAULT_TUTORIALS_DIR).strip("/"),
|
|
79
|
+
notebooks_branch=_setting(nma, "notebooks", "branch", DEFAULT_NOTEBOOKS_BRANCH),
|
|
80
|
+
nma=nma,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_settings(path: Path = MYST_CONFIG) -> Settings:
|
|
85
|
+
path = Path(path)
|
|
86
|
+
if not path.is_file():
|
|
87
|
+
raise MissingConfig(path)
|
|
88
|
+
return load_settings(path.read_text(encoding="utf-8"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def page_location(settings: Settings, page: str | Path) -> tuple[str, str] | None:
|
|
92
|
+
"""(rel_dir, stem) of a page path relative to the course root, or None if
|
|
93
|
+
it is outside the tutorials dir. 'tutorials/W1/T1.md' -> ('W1', 'T1')."""
|
|
94
|
+
path = PurePosixPath(Path(page).as_posix())
|
|
95
|
+
try:
|
|
96
|
+
rel = path.relative_to(settings.tutorials_dir)
|
|
97
|
+
except ValueError:
|
|
98
|
+
return None
|
|
99
|
+
rel_dir = rel.parent.as_posix()
|
|
100
|
+
return ("" if rel_dir == "." else rel_dir), rel.stem
|