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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdevAuto
3
- Version: 0.3.65
3
+ Version: 0.3.66
4
4
  Summary: automating nbdev
5
5
  Author-email: Benedict Thekkel <bthekkel1@gmail.com>
6
6
  License-Expression: Apache-2.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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdevAuto
3
- Version: 0.3.65
3
+ Version: 0.3.66
4
4
  Summary: automating nbdev
5
5
  Author-email: Benedict Thekkel <bthekkel1@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -6,6 +6,7 @@ nbdevAuto/__init__.py
6
6
  nbdevAuto/_modidx.py
7
7
  nbdevAuto/automate.py
8
8
  nbdevAuto/core.py
9
+ nbdevAuto/fleet.py
9
10
  nbdevAuto/functions.py
10
11
  nbdevAuto/github.py
11
12
  nbdevAuto/pdf.py
@@ -1,4 +1,5 @@
1
1
  [console_scripts]
2
+ fleet = nbdevAuto.fleet:fleet
2
3
  gacp = nbdevAuto.automate:gacp
3
4
  ghstatus = nbdevAuto.github:ghstatus
4
5
  gitrelease = nbdevAuto.automate:release_git
@@ -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