nbdevAuto 0.3.64__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.64/nbdevAuto.egg-info → nbdevauto-0.3.66}/PKG-INFO +1 -1
- nbdevauto-0.3.66/nbdevAuto/__init__.py +1 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto/_modidx.py +32 -1
- nbdevauto-0.3.66/nbdevAuto/automate.py +341 -0
- nbdevauto-0.3.66/nbdevAuto/fleet.py +201 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66/nbdevAuto.egg-info}/PKG-INFO +1 -1
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto.egg-info/SOURCES.txt +1 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto.egg-info/entry_points.txt +1 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/pyproject.toml +1 -0
- nbdevauto-0.3.64/nbdevAuto/__init__.py +0 -1
- nbdevauto-0.3.64/nbdevAuto/automate.py +0 -209
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/LICENSE +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/MANIFEST.in +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/README.md +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto/core.py +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto/functions.py +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto/github.py +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto/pdf.py +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto.egg-info/dependency_links.txt +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto.egg-info/requires.txt +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/nbdevAuto.egg-info/top_level.txt +0 -0
- {nbdevauto-0.3.64 → nbdevauto-0.3.66}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.66"
|
|
@@ -5,11 +5,23 @@ d = { 'settings': { 'branch': 'main',
|
|
|
5
5
|
'doc_host': 'https://bthek1.github.io',
|
|
6
6
|
'git_url': 'https://github.com/bthek1/nbdevAuto',
|
|
7
7
|
'lib_path': 'nbdevAuto'},
|
|
8
|
-
'syms': { 'nbdevAuto.automate': { 'nbdevAuto.automate.
|
|
8
|
+
'syms': { 'nbdevAuto.automate': { 'nbdevAuto.automate.StepFailed': ('automate.html#stepfailed', 'nbdevAuto/automate.py'),
|
|
9
|
+
'nbdevAuto.automate.StepFailed.__init__': ( 'automate.html#stepfailed.__init__',
|
|
10
|
+
'nbdevAuto/automate.py'),
|
|
11
|
+
'nbdevAuto.automate._auto_msg': ('automate.html#_auto_msg', 'nbdevAuto/automate.py'),
|
|
12
|
+
'nbdevAuto.automate._banner': ('automate.html#_banner', 'nbdevAuto/automate.py'),
|
|
13
|
+
'nbdevAuto.automate._commit_summary': ('automate.html#_commit_summary', 'nbdevAuto/automate.py'),
|
|
9
14
|
'nbdevAuto.automate._done': ('automate.html#_done', 'nbdevAuto/automate.py'),
|
|
10
15
|
'nbdevAuto.automate._dur': ('automate.html#_dur', 'nbdevAuto/automate.py'),
|
|
16
|
+
'nbdevAuto.automate._push_summary': ('automate.html#_push_summary', 'nbdevAuto/automate.py'),
|
|
17
|
+
'nbdevAuto.automate._quiet': ('automate.html#_quiet', 'nbdevAuto/automate.py'),
|
|
18
|
+
'nbdevAuto.automate._rule': ('automate.html#_rule', 'nbdevAuto/automate.py'),
|
|
19
|
+
'nbdevAuto.automate._run': ('automate.html#_run', 'nbdevAuto/automate.py'),
|
|
20
|
+
'nbdevAuto.automate._skip': ('automate.html#_skip', 'nbdevAuto/automate.py'),
|
|
11
21
|
'nbdevAuto.automate._status_table': ('automate.html#_status_table', 'nbdevAuto/automate.py'),
|
|
12
22
|
'nbdevAuto.automate._step': ('automate.html#_step', 'nbdevAuto/automate.py'),
|
|
23
|
+
'nbdevAuto.automate._test_summary': ('automate.html#_test_summary', 'nbdevAuto/automate.py'),
|
|
24
|
+
'nbdevAuto.automate._version_summary': ('automate.html#_version_summary', 'nbdevAuto/automate.py'),
|
|
13
25
|
'nbdevAuto.automate.gacp': ('automate.html#gacp', 'nbdevAuto/automate.py'),
|
|
14
26
|
'nbdevAuto.automate.help_output': ('automate.html#help_output', 'nbdevAuto/automate.py'),
|
|
15
27
|
'nbdevAuto.automate.prep': ('automate.html#prep', 'nbdevAuto/automate.py'),
|
|
@@ -18,6 +30,25 @@ d = { 'settings': { 'branch': 'main',
|
|
|
18
30
|
'nbdevAuto.automate.release_pypi': ('automate.html#release_pypi', 'nbdevAuto/automate.py'),
|
|
19
31
|
'nbdevAuto.automate.status': ('automate.html#status', 'nbdevAuto/automate.py'),
|
|
20
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')},
|
|
21
52
|
'nbdevAuto.functions': { 'nbdevAuto.functions.classify_images': ('functions.html#classify_images', 'nbdevAuto/functions.py'),
|
|
22
53
|
'nbdevAuto.functions.create_data_folder': ( 'functions.html#create_data_folder',
|
|
23
54
|
'nbdevAuto/functions.py'),
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Automate
|
|
2
|
+
|
|
3
|
+
Docs: https://bthek1.github.io/nbdevAuto/automate.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_Automate.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['prep', 'gacp', 'status', 'upload', 'release_git', 'release_pypi', 'release', 'help_output']
|
|
9
|
+
|
|
10
|
+
# %% ../nbs/01_Automate.ipynb #6d1499d1-aa55-47d8-92fd-fa056b1fb135
|
|
11
|
+
from fastcore.script import *
|
|
12
|
+
from rich import box
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.markup import escape
|
|
15
|
+
from rich.rule import Rule
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
# %% ../nbs/01_Automate.ipynb #7c1e5a90-3f52-4d21-9b6a-2c0f4e8d1a37
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
from contextlib import contextmanager, redirect_stderr, redirect_stdout
|
|
22
|
+
from io import StringIO
|
|
23
|
+
from time import perf_counter
|
|
24
|
+
|
|
25
|
+
from nbdevAuto import __version__
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
# A rule drawn across a 200-column terminal is a wall of dashes. 88 reads as a header.
|
|
30
|
+
_RULE_W = 88
|
|
31
|
+
|
|
32
|
+
# `git status -s` codes: colour, the word for the third column, and the verb a commit
|
|
33
|
+
# subject gets when every staged path carries that same code.
|
|
34
|
+
_CODE_STYLE = {"M":"yellow", "A":"green", "D":"red", "R":"cyan", "C":"cyan", "U":"magenta", "?":"dim"}
|
|
35
|
+
_CODE_LABEL = {"M":"modified", "A":"added", "D":"deleted", "R":"renamed", "C":"copied",
|
|
36
|
+
"U":"conflict", "?":"untracked"}
|
|
37
|
+
_CODE_VERB = {"M":"update", "A":"add", "D":"remove", "R":"rename", "C":"copy",
|
|
38
|
+
"U":"merge", "?":"add"}
|
|
39
|
+
|
|
40
|
+
_RE_VERSION = re.compile(r"^(Old|New) version:\s*(\S+)", re.M)
|
|
41
|
+
_RE_TIMING = re.compile(r"^(?P<nb>.+?):\s+(?P<secs>[\d.]+)\s*secs?$")
|
|
42
|
+
_RE_COMMIT = re.compile(r"^\[(?P<branch>\S+)\s+(?P<sha>[0-9a-f]+)\]")
|
|
43
|
+
_RE_COUNTS = re.compile(r"(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?")
|
|
44
|
+
_RE_WROTE = re.compile(r"Writing objects:.*?,\s*(?P<size>[\d.]+\s*[KMG]?i?B)")
|
|
45
|
+
|
|
46
|
+
class StepFailed(Exception):
|
|
47
|
+
"A stage exited non-zero. Carries the output so `_step` can show what went wrong"
|
|
48
|
+
def __init__(self, cmd, code, output=""):
|
|
49
|
+
self.cmd, self.code, self.output = cmd, code, output
|
|
50
|
+
super().__init__(f"{cmd} exited {code}")
|
|
51
|
+
|
|
52
|
+
def _dur(t0):
|
|
53
|
+
"Elapsed time since `t0`, at a readable scale"
|
|
54
|
+
s = perf_counter() - t0
|
|
55
|
+
if s < 1: return f"{s*1000:.0f}ms"
|
|
56
|
+
if s < 60: return f"{s:.1f}s"
|
|
57
|
+
return f"{int(s)//60}m{int(s)%60:02d}s"
|
|
58
|
+
|
|
59
|
+
def _rule(text, style):
|
|
60
|
+
"A rule capped at `_RULE_W`; `console.rule` otherwise spans the whole terminal"
|
|
61
|
+
console.print(Rule(text, style=style), width=min(console.width, _RULE_W))
|
|
62
|
+
|
|
63
|
+
def _banner(cmd):
|
|
64
|
+
"Command header, with the package version alongside"
|
|
65
|
+
_rule(f"[bold cyan]{cmd}[/bold cyan] [dim]nbdevAuto {__version__}[/dim]", "cyan")
|
|
66
|
+
|
|
67
|
+
def _done(cmd, t0):
|
|
68
|
+
"Closing summary for a multi-stage command"
|
|
69
|
+
_rule(f"[green]{cmd} finished[/green] [dim]in {_dur(t0)}[/dim]", "green")
|
|
70
|
+
|
|
71
|
+
@contextmanager
|
|
72
|
+
def _step(n, total, label):
|
|
73
|
+
"""Run one stage, reporting its outcome and duration.
|
|
74
|
+
|
|
75
|
+
Yields a list: append short strings and they print under the step, which is how a
|
|
76
|
+
stage says what it did without letting the underlying tool print it all. Re-raises
|
|
77
|
+
so nothing is swallowed, and a `StepFailed` shows the output it carries."""
|
|
78
|
+
console.print(f"[dim]{n}/{total}[/dim] [bold]{label}[/bold]")
|
|
79
|
+
t0, detail = perf_counter(), []
|
|
80
|
+
try:
|
|
81
|
+
yield detail
|
|
82
|
+
except StepFailed as e:
|
|
83
|
+
console.print(f" [bold red]FAIL[/bold red] [dim]{_dur(t0)}, exit {e.code}[/dim]")
|
|
84
|
+
for line in e.output.splitlines():
|
|
85
|
+
if line.strip(): console.print(f" [red]{escape(line.rstrip())}[/red]")
|
|
86
|
+
console.print()
|
|
87
|
+
raise
|
|
88
|
+
except BaseException:
|
|
89
|
+
console.print(f" [bold red]FAIL[/bold red] [dim]{_dur(t0)}[/dim]\n")
|
|
90
|
+
raise
|
|
91
|
+
for line in detail: console.print(f" [dim]{escape(line)}[/dim]")
|
|
92
|
+
console.print(f" [green]OK[/green] [dim]{_dur(t0)}[/dim]\n")
|
|
93
|
+
|
|
94
|
+
def _skip(n, total, label, why):
|
|
95
|
+
"Report a stage that was deliberately not run"
|
|
96
|
+
console.print(f"[dim]{n}/{total}[/dim] [bold]{label}[/bold]")
|
|
97
|
+
console.print(f" [yellow]SKIP[/yellow] [dim]{why}[/dim]\n")
|
|
98
|
+
|
|
99
|
+
def _run(*cmd):
|
|
100
|
+
"""Run `cmd` with output captured, returning stdout+stderr.
|
|
101
|
+
|
|
102
|
+
A bare `subprocess.run` returns an exit code nobody reads, so a failed `git push`
|
|
103
|
+
still printed a green OK. Raising is what makes that OK mean something."""
|
|
104
|
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
105
|
+
out = f"{r.stdout or ''}{r.stderr or ''}"
|
|
106
|
+
if r.returncode: raise StepFailed(" ".join(cmd), r.returncode, out)
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
@contextmanager
|
|
110
|
+
def _quiet():
|
|
111
|
+
"Collect what a wrapped nbdev call prints, so the step can summarise it instead"
|
|
112
|
+
buf = StringIO()
|
|
113
|
+
try:
|
|
114
|
+
with redirect_stdout(buf), redirect_stderr(buf): yield buf
|
|
115
|
+
except BaseException:
|
|
116
|
+
# A failing stage is the one time every raw line is worth having.
|
|
117
|
+
text = buf.getvalue().strip()
|
|
118
|
+
if text: console.print(f"[dim]{escape(text)}[/dim]")
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
def _version_summary(out):
|
|
122
|
+
"The two lines `nbdev_bump_version` prints, as one arrow"
|
|
123
|
+
v = dict(_RE_VERSION.findall(out))
|
|
124
|
+
if "Old" in v and "New" in v: return [f"{v['Old']} -> {v['New']}"]
|
|
125
|
+
return [l.strip() for l in out.splitlines() if l.strip()]
|
|
126
|
+
|
|
127
|
+
def _test_summary(out, slow_secs=1.0, keep=3):
|
|
128
|
+
"`nbdev_test` prints a line per notebook; keep the count and only the genuinely slow ones"
|
|
129
|
+
times = []
|
|
130
|
+
for line in out.splitlines():
|
|
131
|
+
m = _RE_TIMING.match(line.strip())
|
|
132
|
+
if m:
|
|
133
|
+
try: times.append((m["nb"], float(m["secs"])))
|
|
134
|
+
except ValueError: pass
|
|
135
|
+
if not times: return [l.strip() for l in out.splitlines() if l.strip()][-3:]
|
|
136
|
+
head = f"{len(times)} notebook{'' if len(times) == 1 else 's'}"
|
|
137
|
+
slow = [t for t in sorted(times, key=lambda t: -t[1]) if t[1] >= slow_secs][:keep]
|
|
138
|
+
if not slow: return [f"{head}, none slower than {slow_secs:g}s"]
|
|
139
|
+
return [f"{head}, slowest:"] + [f" {nb} {s:.1f}s" for nb, s in slow]
|
|
140
|
+
|
|
141
|
+
def _commit_summary(out):
|
|
142
|
+
"Where the commit landed and how big it was, without echoing the subject back"
|
|
143
|
+
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
|
144
|
+
if not lines: return []
|
|
145
|
+
m, c = _RE_COMMIT.match(lines[0]), _RE_COUNTS.search(out)
|
|
146
|
+
head = f"{m['branch']} {m['sha']}" if m else lines[0]
|
|
147
|
+
if not c: return [head]
|
|
148
|
+
files, ins, dels = c.group(1), c.group(2) or "0", c.group(3) or "0"
|
|
149
|
+
return [f"{head} {files} file{'' if files == '1' else 's'}, +{ins} -{dels}"]
|
|
150
|
+
|
|
151
|
+
def _push_summary(out):
|
|
152
|
+
"What moved and how much went over the wire; the progress meters are noise"
|
|
153
|
+
lines = [l.rstrip() for l in out.splitlines() if l.strip()]
|
|
154
|
+
if not lines: return ["nothing to push"]
|
|
155
|
+
if any("Everything up-to-date" in l for l in lines): return ["everything up-to-date"]
|
|
156
|
+
detail = []
|
|
157
|
+
for i, l in enumerate(lines):
|
|
158
|
+
if l.startswith("To ") and i + 1 < len(lines):
|
|
159
|
+
detail += [lines[i + 1].strip(), f"to {l[3:].strip()}"]
|
|
160
|
+
break
|
|
161
|
+
m = _RE_WROTE.search(out)
|
|
162
|
+
if m: detail.append(f"{m['size'].strip()} written")
|
|
163
|
+
return detail or lines[-1:]
|
|
164
|
+
|
|
165
|
+
def _auto_msg(porcelain, limit=3):
|
|
166
|
+
"A commit subject built from the staged paths. The raw status output made a poor one"
|
|
167
|
+
rows = [(l[:2].strip()[:1], l[3:].strip()) for l in porcelain.splitlines() if l.strip()]
|
|
168
|
+
if not rows: return "update"
|
|
169
|
+
verb = _CODE_VERB.get(rows[0][0], "update") if len({c for c, _ in rows}) == 1 else "update"
|
|
170
|
+
names = [p.split(" -> ")[-1] for _, p in rows]
|
|
171
|
+
head = ", ".join(names[:limit])
|
|
172
|
+
if len(names) > limit: head += f" and {len(names) - limit} more"
|
|
173
|
+
return f"{verb} {head}"
|
|
174
|
+
|
|
175
|
+
def _status_table(porcelain):
|
|
176
|
+
"Render `git status -s` output as a table, one row per path"
|
|
177
|
+
t = Table(box=box.SIMPLE, show_header=False, pad_edge=False, expand=False)
|
|
178
|
+
t.add_column("code", no_wrap=True, justify="right")
|
|
179
|
+
t.add_column("path", overflow="fold")
|
|
180
|
+
t.add_column("what", style="dim", no_wrap=True)
|
|
181
|
+
n = 0
|
|
182
|
+
for line in porcelain.splitlines():
|
|
183
|
+
if not line.strip(): continue
|
|
184
|
+
code, path = line[:2].strip(), line[3:]
|
|
185
|
+
style = _CODE_STYLE.get(code[:1], "white")
|
|
186
|
+
t.add_row(f"[{style}]{code}[/{style}]", escape(path), _CODE_LABEL.get(code[:1], ""))
|
|
187
|
+
n += 1
|
|
188
|
+
return t, n
|
|
189
|
+
|
|
190
|
+
# %% ../nbs/01_Automate.ipynb #3a6f81d2-5c47-4e19-8b03-9d2a7f6c0e15
|
|
191
|
+
@call_parse
|
|
192
|
+
def prep(
|
|
193
|
+
p:int = 2, # Increment Part
|
|
194
|
+
):
|
|
195
|
+
"Bump version part `p`, then export, test and clean the notebooks, refreshing _quarto.yml and README"
|
|
196
|
+
|
|
197
|
+
import nbdev.test, nbdev.clean, nbdev.quarto, nbdev.release
|
|
198
|
+
_banner("prep")
|
|
199
|
+
t0 = perf_counter()
|
|
200
|
+
with _step(1, 6, f"bump version (part {p})") as d, _quiet() as out:
|
|
201
|
+
nbdev.release.nbdev_bump_version(p)
|
|
202
|
+
d += _version_summary(out.getvalue())
|
|
203
|
+
with _step(2, 6, "nbdev_export"), _quiet():
|
|
204
|
+
nbdev.quarto.nbdev_export.__wrapped__()
|
|
205
|
+
with _step(3, 6, "nbdev_test") as d, _quiet() as out:
|
|
206
|
+
nbdev.test.nbdev_test.__wrapped__(
|
|
207
|
+
n_workers = 8, # Number of workers
|
|
208
|
+
timing = True, # Time each notebook to see which are slow
|
|
209
|
+
)
|
|
210
|
+
d += _test_summary(out.getvalue())
|
|
211
|
+
with _step(4, 6, "nbdev_clean"), _quiet():
|
|
212
|
+
nbdev.clean.nbdev_clean.__wrapped__()
|
|
213
|
+
with _step(5, 6, "refresh_quarto_yml"), _quiet():
|
|
214
|
+
nbdev.quarto.refresh_quarto_yml()
|
|
215
|
+
with _step(6, 6, "nbdev_readme"), _quiet():
|
|
216
|
+
nbdev.quarto.nbdev_readme.__wrapped__(chk_time=True)
|
|
217
|
+
_done("prep", t0)
|
|
218
|
+
|
|
219
|
+
# %% ../nbs/01_Automate.ipynb #f66ef35f-b7fa-4039-aa07-e9b782d204bb
|
|
220
|
+
@call_parse
|
|
221
|
+
def gacp(
|
|
222
|
+
m:str = '', # Commit message
|
|
223
|
+
):
|
|
224
|
+
"git add, commit and push. Without `-m` the message is built from the staged paths"
|
|
225
|
+
|
|
226
|
+
_banner("gacp")
|
|
227
|
+
t0 = perf_counter()
|
|
228
|
+
with _step(1, 3, "git add"):
|
|
229
|
+
_run("git", "add", ".")
|
|
230
|
+
# NOT via `_run`: `git status -s` pads the code to two columns, and the capture has
|
|
231
|
+
# to keep that leading space or the first path shifts by one.
|
|
232
|
+
status = subprocess.check_output(["git", "status", "-s"]).decode('utf-8')
|
|
233
|
+
staged = bool(status.strip())
|
|
234
|
+
if staged:
|
|
235
|
+
table, n = _status_table(status)
|
|
236
|
+
console.print(table)
|
|
237
|
+
console.print(f"[dim]{n} path{'' if n == 1 else 's'} staged[/dim]\n")
|
|
238
|
+
else:
|
|
239
|
+
console.print("[dim]nothing staged, the tree is clean[/dim]\n")
|
|
240
|
+
|
|
241
|
+
if staged:
|
|
242
|
+
with _step(2, 3, "git commit") as d:
|
|
243
|
+
d += _commit_summary(_run("git", "commit", "-m", m if m != '' else _auto_msg(status)))
|
|
244
|
+
else:
|
|
245
|
+
# `git commit` exits 1 with nothing staged, which is now a hard failure rather
|
|
246
|
+
# than a silent one. There may still be unpushed commits, so go on to the push.
|
|
247
|
+
_skip(2, 3, "git commit", "nothing staged")
|
|
248
|
+
with _step(3, 3, "git push") as d:
|
|
249
|
+
d += _push_summary(_run("git", "push"))
|
|
250
|
+
_done("gacp", t0)
|
|
251
|
+
|
|
252
|
+
# %% ../nbs/01_Automate.ipynb #88017a1b-55c1-4833-aeb6-277a7e5b6314
|
|
253
|
+
def status():
|
|
254
|
+
"Show the working tree state"
|
|
255
|
+
import subprocess
|
|
256
|
+
|
|
257
|
+
def _git(*a, **kw): return subprocess.check_output(["git", *a], **kw).decode('utf-8').strip()
|
|
258
|
+
|
|
259
|
+
_banner("status")
|
|
260
|
+
branch = _git("branch", "--show-current") or "DETACHED"
|
|
261
|
+
line = f"on [bold]{branch}[/bold]"
|
|
262
|
+
try:
|
|
263
|
+
behind, ahead = _git("rev-list", "--left-right", "--count", "@{upstream}...HEAD",
|
|
264
|
+
stderr=subprocess.DEVNULL).split()
|
|
265
|
+
if behind != "0": line += f" [yellow]behind {behind}[/yellow]"
|
|
266
|
+
if ahead != "0": line += f" [cyan]ahead {ahead}[/cyan]"
|
|
267
|
+
if behind == ahead == "0": line += " [dim]in step with upstream[/dim]"
|
|
268
|
+
except subprocess.CalledProcessError:
|
|
269
|
+
line += " [dim]no upstream[/dim]"
|
|
270
|
+
console.print(line)
|
|
271
|
+
|
|
272
|
+
# NOT via _git: `git status -s` pads the code to two columns, so stripping the
|
|
273
|
+
# output would eat the leading space of the first line and shift its path by one.
|
|
274
|
+
porcelain = subprocess.check_output(["git", "status", "-s"]).decode('utf-8')
|
|
275
|
+
if not porcelain:
|
|
276
|
+
console.print("[green]clean[/green]")
|
|
277
|
+
return
|
|
278
|
+
table, n = _status_table(porcelain)
|
|
279
|
+
console.print(table)
|
|
280
|
+
console.print(f"[dim]{n} path{'' if n == 1 else 's'} changed[/dim]")
|
|
281
|
+
|
|
282
|
+
# %% ../nbs/01_Automate.ipynb #8d777a41-0160-47f8-b51c-b14360b62da4
|
|
283
|
+
@call_parse
|
|
284
|
+
def upload(
|
|
285
|
+
m:str = '', # Commit message
|
|
286
|
+
p:int = 2, # Increment part
|
|
287
|
+
):
|
|
288
|
+
"prep then gacp: the everyday command for shipping a notebook change"
|
|
289
|
+
prep(p)
|
|
290
|
+
gacp(m)
|
|
291
|
+
|
|
292
|
+
# %% ../nbs/01_Automate.ipynb #dee37b06-c946-4b40-be0f-056e133751ea
|
|
293
|
+
def release_git():
|
|
294
|
+
"Bump the MINOR version, then tag and create a GitHub release"
|
|
295
|
+
import nbdev.release
|
|
296
|
+
_banner("gitrelease")
|
|
297
|
+
t0 = perf_counter()
|
|
298
|
+
with _step(1, 2, "bump minor version") as d, _quiet() as out:
|
|
299
|
+
nbdev.release.nbdev_bump_version(1)
|
|
300
|
+
d += _version_summary(out.getvalue())
|
|
301
|
+
with _step(2, 2, "tag and create the GitHub release"):
|
|
302
|
+
nbdev.release.release_git()
|
|
303
|
+
_done("gitrelease", t0)
|
|
304
|
+
|
|
305
|
+
# %% ../nbs/01_Automate.ipynb #288708c6-194a-4059-9ff0-ae330079c199
|
|
306
|
+
def release_pypi():
|
|
307
|
+
"Build the sdist and wheel, then upload to PyPI with twine. CI publishes on push, so this is the manual path"
|
|
308
|
+
import nbdev.release
|
|
309
|
+
_banner("piprelease")
|
|
310
|
+
t0 = perf_counter()
|
|
311
|
+
# Deliberately not quieted: a twine upload is worth watching line by line.
|
|
312
|
+
with _step(1, 1, "build and upload to PyPI"):
|
|
313
|
+
nbdev.release.release_pypi()
|
|
314
|
+
_done("piprelease", t0)
|
|
315
|
+
|
|
316
|
+
# %% ../nbs/01_Automate.ipynb #dd798178-9d58-457e-969a-d86a5e8bf875
|
|
317
|
+
def release():
|
|
318
|
+
"release_git then release_pypi"
|
|
319
|
+
release_git()
|
|
320
|
+
release_pypi()
|
|
321
|
+
|
|
322
|
+
# %% ../nbs/01_Automate.ipynb #c4d2bd52-04c4-43fe-890b-b4cb5da8d206
|
|
323
|
+
def help_output():
|
|
324
|
+
"Print every console script this package installs, with its help"
|
|
325
|
+
from importlib.metadata import distribution
|
|
326
|
+
try:
|
|
327
|
+
eps = [e for e in distribution("nbdevAuto").entry_points if e.group == "console_scripts"]
|
|
328
|
+
except Exception:
|
|
329
|
+
# Running from a source tree with nothing installed: fall back to fastcore.
|
|
330
|
+
from fastcore.xtras import console_help
|
|
331
|
+
return console_help('nbdevAuto')
|
|
332
|
+
_banner("commands")
|
|
333
|
+
t = Table(box=box.SIMPLE, show_header=False, pad_edge=False, expand=False)
|
|
334
|
+
t.add_column("command", style="bold cyan", no_wrap=True)
|
|
335
|
+
t.add_column("does", overflow="fold")
|
|
336
|
+
for e in sorted(eps, key=lambda e: e.name):
|
|
337
|
+
try: doc = escape((e.load().__doc__ or "").strip().splitlines()[0])
|
|
338
|
+
except Exception: doc = "[red]could not load[/red]"
|
|
339
|
+
t.add_row(e.name, doc)
|
|
340
|
+
console.print(t)
|
|
341
|
+
console.print(escape("ghstatus needs the gh extra: pip install 'nbdevAuto[gh]'"), style="dim")
|
|
@@ -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.64"
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
"""Automate
|
|
2
|
-
|
|
3
|
-
Docs: https://bthek1.github.io/nbdevAuto/automate.html.md"""
|
|
4
|
-
|
|
5
|
-
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_Automate.ipynb.
|
|
6
|
-
|
|
7
|
-
# %% auto #0
|
|
8
|
-
__all__ = ['prep', 'gacp', 'status', 'upload', 'release_git', 'release_pypi', 'release', 'help_output']
|
|
9
|
-
|
|
10
|
-
# %% ../nbs/01_Automate.ipynb #6d1499d1-aa55-47d8-92fd-fa056b1fb135
|
|
11
|
-
from fastcore.script import *
|
|
12
|
-
from rich import box
|
|
13
|
-
from rich.console import Console
|
|
14
|
-
from rich.markup import escape
|
|
15
|
-
from rich.table import Table
|
|
16
|
-
|
|
17
|
-
# %% ../nbs/01_Automate.ipynb #7c1e5a90-3f52-4d21-9b6a-2c0f4e8d1a37
|
|
18
|
-
from contextlib import contextmanager
|
|
19
|
-
from time import perf_counter
|
|
20
|
-
|
|
21
|
-
from nbdevAuto import __version__
|
|
22
|
-
|
|
23
|
-
console = Console()
|
|
24
|
-
|
|
25
|
-
# `git status -s` codes: colour, and the word for the third column.
|
|
26
|
-
_CODE_STYLE = {"M":"yellow", "A":"green", "D":"red", "R":"cyan", "C":"cyan", "U":"magenta", "?":"dim"}
|
|
27
|
-
_CODE_LABEL = {"M":"modified", "A":"added", "D":"deleted", "R":"renamed", "C":"copied",
|
|
28
|
-
"U":"conflict", "?":"untracked"}
|
|
29
|
-
|
|
30
|
-
def _dur(t0):
|
|
31
|
-
"Elapsed time since `t0`, at a readable scale"
|
|
32
|
-
s = perf_counter() - t0
|
|
33
|
-
if s < 1: return f"{s*1000:.0f}ms"
|
|
34
|
-
if s < 60: return f"{s:.1f}s"
|
|
35
|
-
return f"{int(s)//60}m{int(s)%60:02d}s"
|
|
36
|
-
|
|
37
|
-
def _banner(cmd):
|
|
38
|
-
"Command header, with the package version alongside"
|
|
39
|
-
console.rule(f"[bold cyan]{cmd}[/bold cyan] [dim]nbdevAuto {__version__}[/dim]", style="cyan")
|
|
40
|
-
|
|
41
|
-
def _done(cmd, t0):
|
|
42
|
-
"Closing summary for a multi-stage command"
|
|
43
|
-
console.rule(f"[green]{cmd} finished[/green] [dim]in {_dur(t0)}[/dim]", style="green")
|
|
44
|
-
|
|
45
|
-
@contextmanager
|
|
46
|
-
def _step(n, total, label):
|
|
47
|
-
"Run one stage, reporting its outcome and duration. Re-raises so nothing is swallowed"
|
|
48
|
-
console.print(f"[dim]{n}/{total}[/dim] [bold]{label}[/bold]")
|
|
49
|
-
t0 = perf_counter()
|
|
50
|
-
try:
|
|
51
|
-
yield
|
|
52
|
-
except BaseException:
|
|
53
|
-
console.print(f" [bold red]FAIL[/bold red] [dim]{_dur(t0)}[/dim]\n")
|
|
54
|
-
raise
|
|
55
|
-
console.print(f" [green]OK[/green] [dim]{_dur(t0)}[/dim]\n")
|
|
56
|
-
|
|
57
|
-
def _status_table(porcelain):
|
|
58
|
-
"Render `git status -s` output as a table, one row per path"
|
|
59
|
-
t = Table(box=box.SIMPLE, show_header=False, pad_edge=False, expand=False)
|
|
60
|
-
t.add_column("code", no_wrap=True, justify="right")
|
|
61
|
-
t.add_column("path", overflow="fold")
|
|
62
|
-
t.add_column("what", style="dim", no_wrap=True)
|
|
63
|
-
n = 0
|
|
64
|
-
for line in porcelain.splitlines():
|
|
65
|
-
if not line.strip(): continue
|
|
66
|
-
code, path = line[:2].strip(), line[3:]
|
|
67
|
-
style = _CODE_STYLE.get(code[:1], "white")
|
|
68
|
-
t.add_row(f"[{style}]{code}[/{style}]", escape(path), _CODE_LABEL.get(code[:1], ""))
|
|
69
|
-
n += 1
|
|
70
|
-
return t, n
|
|
71
|
-
|
|
72
|
-
# %% ../nbs/01_Automate.ipynb #3a6f81d2-5c47-4e19-8b03-9d2a7f6c0e15
|
|
73
|
-
@call_parse
|
|
74
|
-
def prep(
|
|
75
|
-
p:int = 2, # Increment Part
|
|
76
|
-
):
|
|
77
|
-
"Bump version part `p`, then export, test and clean the notebooks, refreshing _quarto.yml and README"
|
|
78
|
-
|
|
79
|
-
import nbdev.test, nbdev.clean, nbdev.quarto, nbdev.release
|
|
80
|
-
_banner("prep")
|
|
81
|
-
t0 = perf_counter()
|
|
82
|
-
with _step(1, 6, f"bump version (part {p})"):
|
|
83
|
-
nbdev.release.nbdev_bump_version(p)
|
|
84
|
-
with _step(2, 6, "nbdev_export"):
|
|
85
|
-
nbdev.quarto.nbdev_export.__wrapped__()
|
|
86
|
-
with _step(3, 6, "nbdev_test"):
|
|
87
|
-
nbdev.test.nbdev_test.__wrapped__(
|
|
88
|
-
n_workers = 8, # Number of workers
|
|
89
|
-
timing = True, # Time each notebook to see which are slow
|
|
90
|
-
)
|
|
91
|
-
with _step(4, 6, "nbdev_clean"):
|
|
92
|
-
nbdev.clean.nbdev_clean.__wrapped__()
|
|
93
|
-
with _step(5, 6, "refresh_quarto_yml"):
|
|
94
|
-
nbdev.quarto.refresh_quarto_yml()
|
|
95
|
-
with _step(6, 6, "nbdev_readme"):
|
|
96
|
-
nbdev.quarto.nbdev_readme.__wrapped__(chk_time=True)
|
|
97
|
-
_done("prep", t0)
|
|
98
|
-
|
|
99
|
-
# %% ../nbs/01_Automate.ipynb #f66ef35f-b7fa-4039-aa07-e9b782d204bb
|
|
100
|
-
@call_parse
|
|
101
|
-
def gacp(
|
|
102
|
-
m:str = '', # Commit message
|
|
103
|
-
):
|
|
104
|
-
"git add, commit and push. Without `-m` the commit message is the status output"
|
|
105
|
-
|
|
106
|
-
import subprocess
|
|
107
|
-
_banner("gacp")
|
|
108
|
-
t0 = perf_counter()
|
|
109
|
-
with _step(1, 3, "git add"):
|
|
110
|
-
subprocess.run(["git", "add", "."])
|
|
111
|
-
status = subprocess.check_output(["git", "status", "-s"]).decode('utf-8')
|
|
112
|
-
if status.strip():
|
|
113
|
-
table, n = _status_table(status)
|
|
114
|
-
console.print(table)
|
|
115
|
-
console.print(f"[dim]{n} path{'' if n == 1 else 's'} staged[/dim]\n")
|
|
116
|
-
else:
|
|
117
|
-
console.print("[dim]nothing staged, the tree is clean[/dim]\n")
|
|
118
|
-
|
|
119
|
-
msg = m if m != '' else status
|
|
120
|
-
with _step(2, 3, "git commit"):
|
|
121
|
-
subprocess.run(["git", "commit", "-m", msg])
|
|
122
|
-
with _step(3, 3, "git push"):
|
|
123
|
-
subprocess.run(["git", "push"])
|
|
124
|
-
_done("gacp", t0)
|
|
125
|
-
|
|
126
|
-
# %% ../nbs/01_Automate.ipynb #88017a1b-55c1-4833-aeb6-277a7e5b6314
|
|
127
|
-
def status():
|
|
128
|
-
"Show the working tree state"
|
|
129
|
-
import subprocess
|
|
130
|
-
|
|
131
|
-
def _git(*a, **kw): return subprocess.check_output(["git", *a], **kw).decode('utf-8').strip()
|
|
132
|
-
|
|
133
|
-
_banner("status")
|
|
134
|
-
branch = _git("branch", "--show-current") or "DETACHED"
|
|
135
|
-
line = f"on [bold]{branch}[/bold]"
|
|
136
|
-
try:
|
|
137
|
-
behind, ahead = _git("rev-list", "--left-right", "--count", "@{upstream}...HEAD",
|
|
138
|
-
stderr=subprocess.DEVNULL).split()
|
|
139
|
-
if behind != "0": line += f" [yellow]behind {behind}[/yellow]"
|
|
140
|
-
if ahead != "0": line += f" [cyan]ahead {ahead}[/cyan]"
|
|
141
|
-
if behind == ahead == "0": line += " [dim]in step with upstream[/dim]"
|
|
142
|
-
except subprocess.CalledProcessError:
|
|
143
|
-
line += " [dim]no upstream[/dim]"
|
|
144
|
-
console.print(line)
|
|
145
|
-
|
|
146
|
-
# NOT via _git: `git status -s` pads the code to two columns, so stripping the
|
|
147
|
-
# output would eat the leading space of the first line and shift its path by one.
|
|
148
|
-
porcelain = subprocess.check_output(["git", "status", "-s"]).decode('utf-8')
|
|
149
|
-
if not porcelain:
|
|
150
|
-
console.print("[green]clean[/green]")
|
|
151
|
-
return
|
|
152
|
-
table, n = _status_table(porcelain)
|
|
153
|
-
console.print(table)
|
|
154
|
-
console.print(f"[dim]{n} path{'' if n == 1 else 's'} changed[/dim]")
|
|
155
|
-
|
|
156
|
-
# %% ../nbs/01_Automate.ipynb #8d777a41-0160-47f8-b51c-b14360b62da4
|
|
157
|
-
@call_parse
|
|
158
|
-
def upload(
|
|
159
|
-
m:str = '', # Commit message
|
|
160
|
-
p:int = 2, # Increment part
|
|
161
|
-
):
|
|
162
|
-
"prep then gacp: the everyday command for shipping a notebook change"
|
|
163
|
-
prep(p)
|
|
164
|
-
gacp(m)
|
|
165
|
-
|
|
166
|
-
# %% ../nbs/01_Automate.ipynb #dee37b06-c946-4b40-be0f-056e133751ea
|
|
167
|
-
def release_git():
|
|
168
|
-
"Bump the MINOR version, then tag and create a GitHub release"
|
|
169
|
-
import nbdev.release
|
|
170
|
-
console.rule("[bold]gitrelease")
|
|
171
|
-
with _step("bump minor version"):
|
|
172
|
-
nbdev.release.nbdev_bump_version(1)
|
|
173
|
-
with _step("tag and create the GitHub release"):
|
|
174
|
-
nbdev.release.release_git()
|
|
175
|
-
|
|
176
|
-
# %% ../nbs/01_Automate.ipynb #288708c6-194a-4059-9ff0-ae330079c199
|
|
177
|
-
def release_pypi():
|
|
178
|
-
"Build the sdist and wheel, then upload to PyPI with twine. CI publishes on push, so this is the manual path"
|
|
179
|
-
import nbdev.release
|
|
180
|
-
console.rule("[bold]piprelease")
|
|
181
|
-
with _step("build and upload to PyPI"):
|
|
182
|
-
nbdev.release.release_pypi()
|
|
183
|
-
|
|
184
|
-
# %% ../nbs/01_Automate.ipynb #dd798178-9d58-457e-969a-d86a5e8bf875
|
|
185
|
-
def release():
|
|
186
|
-
"release_git then release_pypi"
|
|
187
|
-
release_git()
|
|
188
|
-
release_pypi()
|
|
189
|
-
|
|
190
|
-
# %% ../nbs/01_Automate.ipynb #c4d2bd52-04c4-43fe-890b-b4cb5da8d206
|
|
191
|
-
def help_output():
|
|
192
|
-
"Print every console script this package installs, with its help"
|
|
193
|
-
from importlib.metadata import distribution
|
|
194
|
-
try:
|
|
195
|
-
eps = [e for e in distribution("nbdevAuto").entry_points if e.group == "console_scripts"]
|
|
196
|
-
except Exception:
|
|
197
|
-
# Running from a source tree with nothing installed: fall back to fastcore.
|
|
198
|
-
from fastcore.xtras import console_help
|
|
199
|
-
return console_help('nbdevAuto')
|
|
200
|
-
_banner("commands")
|
|
201
|
-
t = Table(box=box.SIMPLE, show_header=False, pad_edge=False, expand=False)
|
|
202
|
-
t.add_column("command", style="bold cyan", no_wrap=True)
|
|
203
|
-
t.add_column("does", overflow="fold")
|
|
204
|
-
for e in sorted(eps, key=lambda e: e.name):
|
|
205
|
-
try: doc = escape((e.load().__doc__ or "").strip().splitlines()[0])
|
|
206
|
-
except Exception: doc = "[red]could not load[/red]"
|
|
207
|
-
t.add_row(e.name, doc)
|
|
208
|
-
console.print(t)
|
|
209
|
-
console.print(escape("ghstatus needs the gh extra: pip install 'nbdevAuto[gh]'"), style="dim")
|
|
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
|