nbdevAuto 0.3.65__tar.gz → 0.3.66__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.
- {nbdevauto-0.3.65/nbdevAuto.egg-info → nbdevauto-0.3.66}/PKG-INFO +1 -1
- nbdevauto-0.3.66/nbdevAuto/__init__.py +1 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto/_modidx.py +19 -0
- nbdevauto-0.3.66/nbdevAuto/fleet.py +201 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66/nbdevAuto.egg-info}/PKG-INFO +1 -1
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto.egg-info/SOURCES.txt +1 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto.egg-info/entry_points.txt +1 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/pyproject.toml +1 -0
- nbdevauto-0.3.65/nbdevAuto/__init__.py +0 -1
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/LICENSE +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/MANIFEST.in +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/README.md +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto/automate.py +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto/core.py +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto/functions.py +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto/github.py +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto/pdf.py +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto.egg-info/dependency_links.txt +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto.egg-info/requires.txt +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/nbdevAuto.egg-info/top_level.txt +0 -0
- {nbdevauto-0.3.65 → nbdevauto-0.3.66}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.66"
|
|
@@ -30,6 +30,25 @@ d = { 'settings': { 'branch': 'main',
|
|
|
30
30
|
'nbdevAuto.automate.release_pypi': ('automate.html#release_pypi', 'nbdevAuto/automate.py'),
|
|
31
31
|
'nbdevAuto.automate.status': ('automate.html#status', 'nbdevAuto/automate.py'),
|
|
32
32
|
'nbdevAuto.automate.upload': ('automate.html#upload', 'nbdevAuto/automate.py')},
|
|
33
|
+
'nbdevAuto.fleet': { 'nbdevAuto.fleet.Outcome': ('fleet.html#outcome', 'nbdevAuto/fleet.py'),
|
|
34
|
+
'nbdevAuto.fleet.Report': ('fleet.html#report', 'nbdevAuto/fleet.py'),
|
|
35
|
+
'nbdevAuto.fleet.Report.code': ('fleet.html#report.code', 'nbdevAuto/fleet.py'),
|
|
36
|
+
'nbdevAuto.fleet.Report.failed': ('fleet.html#report.failed', 'nbdevAuto/fleet.py'),
|
|
37
|
+
'nbdevAuto.fleet.Report.ran': ('fleet.html#report.ran', 'nbdevAuto/fleet.py'),
|
|
38
|
+
'nbdevAuto.fleet.Report.skip_counts': ('fleet.html#report.skip_counts', 'nbdevAuto/fleet.py'),
|
|
39
|
+
'nbdevAuto.fleet._classify': ('fleet.html#_classify', 'nbdevAuto/fleet.py'),
|
|
40
|
+
'nbdevAuto.fleet._fail_reason': ('fleet.html#_fail_reason', 'nbdevAuto/fleet.py'),
|
|
41
|
+
'nbdevAuto.fleet._render': ('fleet.html#_render', 'nbdevAuto/fleet.py'),
|
|
42
|
+
'nbdevAuto.fleet.finish': ('fleet.html#finish', 'nbdevAuto/fleet.py'),
|
|
43
|
+
'nbdevAuto.fleet.fleet': ('fleet.html#fleet', 'nbdevAuto/fleet.py'),
|
|
44
|
+
'nbdevAuto.fleet.fleet_upload': ('fleet.html#fleet_upload', 'nbdevAuto/fleet.py'),
|
|
45
|
+
'nbdevAuto.fleet.format_summary': ('fleet.html#format_summary', 'nbdevAuto/fleet.py'),
|
|
46
|
+
'nbdevAuto.fleet.parse_verb': ('fleet.html#parse_verb', 'nbdevAuto/fleet.py'),
|
|
47
|
+
'nbdevAuto.fleet.run_verb': ('fleet.html#run_verb', 'nbdevAuto/fleet.py'),
|
|
48
|
+
'nbdevAuto.fleet.script_path': ('fleet.html#script_path', 'nbdevAuto/fleet.py'),
|
|
49
|
+
'nbdevAuto.fleet.upload_one': ('fleet.html#upload_one', 'nbdevAuto/fleet.py'),
|
|
50
|
+
'nbdevAuto.fleet.usage': ('fleet.html#usage', 'nbdevAuto/fleet.py'),
|
|
51
|
+
'nbdevAuto.fleet.walk': ('fleet.html#walk', 'nbdevAuto/fleet.py')},
|
|
33
52
|
'nbdevAuto.functions': { 'nbdevAuto.functions.classify_images': ('functions.html#classify_images', 'nbdevAuto/functions.py'),
|
|
34
53
|
'nbdevAuto.functions.create_data_folder': ( 'functions.html#create_data_folder',
|
|
35
54
|
'nbdevAuto/functions.py'),
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Run one command across every submodule of a superproject
|
|
2
|
+
|
|
3
|
+
Docs: https://bthek1.github.io/nbdevAuto/fleet.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_Fleet.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['VERBS', 'Outcome', 'Report', 'format_summary', 'walk', 'finish', 'script_path', 'upload_one', 'fleet_upload',
|
|
9
|
+
'parse_verb', 'usage', 'run_verb', 'fleet']
|
|
10
|
+
|
|
11
|
+
# %% ../nbs/04_Fleet.ipynb #d361ee5f-9f09-464f-aec6-0e27cec90fe9
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from fastcore.script import *
|
|
20
|
+
|
|
21
|
+
# The rendering layer lives in `automate`, the git/discovery layer in `github`. This
|
|
22
|
+
# module is the only one that knows about MANY repos; those two stay single-repo.
|
|
23
|
+
from .automate import StepFailed, console, escape
|
|
24
|
+
from .github import find_root, git, parse_gitmodules
|
|
25
|
+
|
|
26
|
+
# %% ../nbs/04_Fleet.ipynb #4e6b24c5-cd71-434c-84e6-e6994d5959c7
|
|
27
|
+
@dataclass
|
|
28
|
+
class Outcome:
|
|
29
|
+
"What happened in one submodule"
|
|
30
|
+
path: str
|
|
31
|
+
state: str # ran | skipped | failed
|
|
32
|
+
reason: str = "" # why it was skipped, or how it failed
|
|
33
|
+
detail: list = field(default_factory=list) # short lines to show under the path
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Report:
|
|
37
|
+
"Everything a `walk` did, and the exit code it implies"
|
|
38
|
+
outcomes: list = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def ran(self): return [o for o in self.outcomes if o.state == "ran"]
|
|
42
|
+
@property
|
|
43
|
+
def failed(self): return [o for o in self.outcomes if o.state == "failed"]
|
|
44
|
+
@property
|
|
45
|
+
def code(self): return 1 if self.failed else 0
|
|
46
|
+
|
|
47
|
+
def skip_counts(self):
|
|
48
|
+
"Skip reason -> count, in the order the reasons were first seen"
|
|
49
|
+
counts = {}
|
|
50
|
+
for o in self.outcomes:
|
|
51
|
+
if o.state == "skipped": counts[o.reason] = counts.get(o.reason, 0) + 1
|
|
52
|
+
return counts
|
|
53
|
+
|
|
54
|
+
def format_summary(rep, verb="ran"):
|
|
55
|
+
"The one line that replaces a message per submodule. Quiet submodules are counted, not narrated"
|
|
56
|
+
n = len(rep.outcomes)
|
|
57
|
+
parts = [f"{len(rep.ran)} {verb}"]
|
|
58
|
+
parts += [f"{c} {reason}" for reason, c in rep.skip_counts().items()]
|
|
59
|
+
if rep.failed: parts.append(f"{len(rep.failed)} FAILED")
|
|
60
|
+
return f"{n} submodule{'' if n == 1 else 's'}: " + ", ".join(parts)
|
|
61
|
+
|
|
62
|
+
# %% ../nbs/04_Fleet.ipynb #099242e8-c9ad-4b48-8ebc-7a818e5f8f81
|
|
63
|
+
def _classify(base, sub, need_nbs, only_dirty):
|
|
64
|
+
"The reason to skip this submodule, or None to run it"
|
|
65
|
+
p = base/sub.path
|
|
66
|
+
# An uninitialised submodule is an empty directory: present, but with no .git
|
|
67
|
+
if not (p/".git").exists(): return "not initialised"
|
|
68
|
+
if need_nbs and not (p/"nbs").is_dir(): return "without nbs"
|
|
69
|
+
if only_dirty and not git(p, "status", "--porcelain"): return "clean"
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def _fail_reason(e):
|
|
73
|
+
"One line describing why a submodule's callable did not finish"
|
|
74
|
+
if isinstance(e, StepFailed): return f"{e.cmd} exited {e.code}"
|
|
75
|
+
return f"{type(e).__name__}: {e}"
|
|
76
|
+
|
|
77
|
+
def walk(fn, *, root=None, only_dirty=False, need_nbs=False, workers=1, header=True):
|
|
78
|
+
"""Run `fn(path, sub)` in each submodule, returning a `Report`.
|
|
79
|
+
|
|
80
|
+
`fn` may return a list of short strings to show under the submodule's name. Only
|
|
81
|
+
submodules that actually do something get printed; the rest are counted by the caller
|
|
82
|
+
through `format_summary`. Sequential runs print the header BEFORE the callable so its
|
|
83
|
+
own output streams under it; parallel runs print after, so lines cannot interleave."""
|
|
84
|
+
base = Path(root) if root else find_root()
|
|
85
|
+
rep, todo = Report(), []
|
|
86
|
+
for s in parse_gitmodules(base):
|
|
87
|
+
why = _classify(base, s, need_nbs, only_dirty)
|
|
88
|
+
if why: rep.outcomes.append(Outcome(s.path, "skipped", why))
|
|
89
|
+
else: todo.append(s)
|
|
90
|
+
|
|
91
|
+
def _one(s):
|
|
92
|
+
try: return Outcome(s.path, "ran", detail=list(fn(base/s.path, s) or []))
|
|
93
|
+
except BaseException as e: return Outcome(s.path, "failed", _fail_reason(e))
|
|
94
|
+
|
|
95
|
+
def _head(path): console.print(f"\n[bold cyan]==> {escape(path)}[/bold cyan]")
|
|
96
|
+
|
|
97
|
+
if workers > 1 and len(todo) > 1:
|
|
98
|
+
# Results are rendered in submodule order, not completion order, so two runs of
|
|
99
|
+
# the same fleet read the same way.
|
|
100
|
+
with ThreadPoolExecutor(max_workers=workers) as ex: done = list(ex.map(_one, todo))
|
|
101
|
+
for o in done:
|
|
102
|
+
if header: _head(o.path)
|
|
103
|
+
_render(o)
|
|
104
|
+
rep.outcomes.append(o)
|
|
105
|
+
else:
|
|
106
|
+
for s in todo:
|
|
107
|
+
if header: _head(s.path)
|
|
108
|
+
o = _one(s)
|
|
109
|
+
_render(o)
|
|
110
|
+
rep.outcomes.append(o)
|
|
111
|
+
rep.outcomes.sort(key=lambda o: o.path)
|
|
112
|
+
return rep
|
|
113
|
+
|
|
114
|
+
def _render(o):
|
|
115
|
+
"A submodule's own lines, or why it failed"
|
|
116
|
+
if o.state == "failed":
|
|
117
|
+
console.print(f" [bold red]FAIL[/bold red] [dim]{escape(o.reason)}[/dim]")
|
|
118
|
+
for line in o.detail: console.print(f" [dim]{escape(line)}[/dim]")
|
|
119
|
+
|
|
120
|
+
def finish(rep, verb):
|
|
121
|
+
"Print the counted summary, name any failures, and return the exit code"
|
|
122
|
+
console.print(f"\n[bold]{escape(format_summary(rep, verb))}[/bold]")
|
|
123
|
+
for o in rep.failed:
|
|
124
|
+
console.print(f"[bold red]FAILED[/bold red] {escape(o.path)}: [dim]{escape(o.reason)}[/dim]")
|
|
125
|
+
return rep.code
|
|
126
|
+
|
|
127
|
+
# %% ../nbs/04_Fleet.ipynb #99e2c4fe-c885-4880-b9df-31da51e61f3a
|
|
128
|
+
def script_path(name):
|
|
129
|
+
"""The console script `name` from the environment this is running in.
|
|
130
|
+
|
|
131
|
+
Taking it from `sys.executable`'s directory is what makes this work under a
|
|
132
|
+
non-interactive shell that never put the venv on PATH."""
|
|
133
|
+
cand = Path(sys.executable).parent/name
|
|
134
|
+
if cand.is_file(): return str(cand)
|
|
135
|
+
found = shutil.which(name)
|
|
136
|
+
if found: return found
|
|
137
|
+
raise RuntimeError(f"Cannot find the '{name}' console script. Run `uv sync` first")
|
|
138
|
+
|
|
139
|
+
def upload_one(path, sub=None, args=()):
|
|
140
|
+
"""Run the single-repo `upload` in `path`, letting its own rich output through.
|
|
141
|
+
|
|
142
|
+
A subprocess rather than an in-process call: `prep` reads nbdev's config from the cwd
|
|
143
|
+
and caches it, and `nbdev_test` spawns its own process pool, so nineteen repos in one
|
|
144
|
+
interpreter is a trap. The subprocess keeps this tty, so colour survives."""
|
|
145
|
+
r = subprocess.run([script_path("upload"), *args], cwd=str(path))
|
|
146
|
+
if r.returncode: raise StepFailed("upload", r.returncode)
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
# %% ../nbs/04_Fleet.ipynb #2ef9f1e5-239a-4790-8cec-43fffb8bc422
|
|
150
|
+
def fleet_upload(
|
|
151
|
+
all:bool = False, # Upload every submodule with an nbs/ folder, not just the changed ones
|
|
152
|
+
root:str = None, # Superproject root (default: nearest ancestor with .gitmodules)
|
|
153
|
+
):
|
|
154
|
+
"Run `upload` in each submodule that has local changes"
|
|
155
|
+
# Sequential on purpose: `upload` runs nbdev_test with 8 workers, and knowledge-lab
|
|
156
|
+
# has 4 vCPU. Parallel uploads would oversubscribe the box and interleave the output.
|
|
157
|
+
rep = walk(upload_one, root=root, need_nbs=True, only_dirty=not all, workers=1)
|
|
158
|
+
sys.exit(finish(rep, "uploaded"))
|
|
159
|
+
|
|
160
|
+
# %% ../nbs/04_Fleet.ipynb #0281bd2b-91ba-4e49-90dc-d1a95d54d5a6
|
|
161
|
+
VERBS = {"upload": fleet_upload}
|
|
162
|
+
|
|
163
|
+
def parse_verb(argv):
|
|
164
|
+
"(verb, remaining args) for `fleet ...`, or None when `argv` does not name a verb"
|
|
165
|
+
if not argv or argv[0].startswith("-"): return None
|
|
166
|
+
return (argv[0], argv[1:]) if argv[0] in VERBS else None
|
|
167
|
+
|
|
168
|
+
def usage():
|
|
169
|
+
"The verb list, for --help and for an unknown verb"
|
|
170
|
+
lines = ["Usage: fleet <verb> [options]", "", "Verbs:"]
|
|
171
|
+
lines += [f" {name:<10} {(fn.__doc__ or '').strip()}" for name, fn in sorted(VERBS.items())]
|
|
172
|
+
lines += ["", "Any verb takes --help for its own options."]
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
def run_verb(name, argv):
|
|
176
|
+
"""Parse `argv` against the verb's own annotations, then call it.
|
|
177
|
+
|
|
178
|
+
Deliberately NOT `@call_parse`: fastcore runs the first decorated function in a module
|
|
179
|
+
the moment that module is `__main__`, which is exactly what makes `python -m
|
|
180
|
+
nbdevAuto.github` work with its single verb. With several verbs it would run whichever
|
|
181
|
+
happened to be defined first, so the verbs stay plain functions and the parser is built
|
|
182
|
+
here. `anno_parser` is the same one `call_parse` uses, so `--help` is unchanged."""
|
|
183
|
+
fn = VERBS[name]
|
|
184
|
+
args = vars(anno_parser(fn, prog=f"fleet {name}").parse_args(argv))
|
|
185
|
+
for k in ("pdb", "xtra"): args.pop(k, None)
|
|
186
|
+
return fn(**args)
|
|
187
|
+
|
|
188
|
+
def fleet():
|
|
189
|
+
"""Dispatch `fleet <verb>` to the matching command.
|
|
190
|
+
|
|
191
|
+
`upload` and `status` are already console scripts bound to the single-repo versions in
|
|
192
|
+
`automate`, so the fleet-wide ones cannot claim those names. One script with verbs
|
|
193
|
+
sidesteps the collision and keeps `--help` working per verb."""
|
|
194
|
+
picked = parse_verb(sys.argv[1:])
|
|
195
|
+
if not picked:
|
|
196
|
+
print(usage())
|
|
197
|
+
sys.exit(0 if sys.argv[1:2] in ([], ["-h"], ["--help"]) else 2)
|
|
198
|
+
return run_verb(*picked)
|
|
199
|
+
|
|
200
|
+
# %% ../nbs/04_Fleet.ipynb #7adb89c6-6788-4241-acff-53cb4566b5ce
|
|
201
|
+
if __name__ == "__main__": fleet()
|
|
@@ -51,6 +51,7 @@ piprelease = "nbdevAuto.automate:release_pypi"
|
|
|
51
51
|
release = "nbdevAuto.automate:release"
|
|
52
52
|
h = "nbdevAuto.automate:help_output"
|
|
53
53
|
ghstatus = "nbdevAuto.github:ghstatus"
|
|
54
|
+
fleet = "nbdevAuto.fleet:fleet"
|
|
54
55
|
|
|
55
56
|
[tool.setuptools.dynamic]
|
|
56
57
|
version = {attr = "nbdevAuto.__version__"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.65"
|
|
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
|