shuttlecheck 0.1.0__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.
- shuttlecheck/__init__.py +39 -0
- shuttlecheck/checks.py +156 -0
- shuttlecheck/cli.py +240 -0
- shuttlecheck/delivery.py +505 -0
- shuttlecheck/extensions.py +53 -0
- shuttlecheck/infer.py +730 -0
- shuttlecheck/patterns.py +350 -0
- shuttlecheck/report.py +146 -0
- shuttlecheck/schedule.py +215 -0
- shuttlecheck/schema.json +1274 -0
- shuttlecheck/sidecar.py +330 -0
- shuttlecheck/spec.py +241 -0
- shuttlecheck/ui.py +332 -0
- shuttlecheck/ui_assets/app.css +186 -0
- shuttlecheck/ui_assets/app.html +145 -0
- shuttlecheck/ui_assets/app.js +396 -0
- shuttlecheck/validate.py +335 -0
- shuttlecheck/version.py +1 -0
- shuttlecheck/walkthrough.py +321 -0
- shuttlecheck-0.1.0.dist-info/METADATA +145 -0
- shuttlecheck-0.1.0.dist-info/RECORD +25 -0
- shuttlecheck-0.1.0.dist-info/WHEEL +5 -0
- shuttlecheck-0.1.0.dist-info/entry_points.txt +2 -0
- shuttlecheck-0.1.0.dist-info/licenses/LICENSE +43 -0
- shuttlecheck-0.1.0.dist-info/top_level.txt +1 -0
shuttlecheck/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""ShuttleCheck -- naming and delivery conformance.
|
|
2
|
+
|
|
3
|
+
Three entry points, in the order you would use them:
|
|
4
|
+
|
|
5
|
+
infer_spec / walkthrough read an existing tree and write the spec it follows
|
|
6
|
+
Validator / validate report what does not conform, in name and in
|
|
7
|
+
substance -- codec, resolution, audio, packages
|
|
8
|
+
verify check a drive against the sidecar it arrived with
|
|
9
|
+
|
|
10
|
+
**Nothing in this package modifies media.** That is the whole of it, and it is
|
|
11
|
+
deliberate: a checker that cannot alter anything is a checker a stranger will
|
|
12
|
+
run on a live job, because running it cannot cost them anything.
|
|
13
|
+
|
|
14
|
+
The operations that write -- conforming, the journal, sealing a drive, vendor
|
|
15
|
+
packs -- are licensed separately and ship as `shuttlecheck_pro`. When it is
|
|
16
|
+
installed, `extensions.load()` finds it and the CLI and app grow the extra
|
|
17
|
+
commands; nothing here imports it, and nothing here requires it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import extensions
|
|
21
|
+
from .delivery import (FFProbeProber, MediaInfo, NullProber, Prober,
|
|
22
|
+
check_deliverable, check_packages, check_sidecars)
|
|
23
|
+
from .infer import InferenceReport, infer_spec
|
|
24
|
+
from .report import Finding, Report
|
|
25
|
+
from .sidecar import (ManifestEntry, Sidecar, SidecarError, read_sidecar,
|
|
26
|
+
render_verify, verify)
|
|
27
|
+
from .spec import Spec, SpecError, check_document, load_spec
|
|
28
|
+
from .validate import Validator, validate
|
|
29
|
+
from .version import __version__
|
|
30
|
+
from .walkthrough import present, questionnaire, render_spec_yaml, walkthrough
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"Finding", "Report", "Spec", "SpecError", "load_spec", "check_document",
|
|
34
|
+
"Validator", "validate", "InferenceReport", "infer_spec", "walkthrough",
|
|
35
|
+
"present", "render_spec_yaml", "questionnaire", "__version__", "extensions",
|
|
36
|
+
"Sidecar", "SidecarError", "ManifestEntry", "verify", "read_sidecar",
|
|
37
|
+
"render_verify", "Prober", "FFProbeProber", "NullProber", "MediaInfo",
|
|
38
|
+
"check_deliverable", "check_packages", "check_sidecars",
|
|
39
|
+
]
|
shuttlecheck/checks.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Portability, structure, and collision checks.
|
|
2
|
+
|
|
3
|
+
Each of these is a real-world failure that looks like a bug in someone else's
|
|
4
|
+
software when it happens: a drive that ingests on macOS and fails on Windows,
|
|
5
|
+
a conform that cannot relink, a copy that silently drops a file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import unicodedata
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .report import Finding
|
|
15
|
+
|
|
16
|
+
RESERVED_RANGE = re.compile(r"^([A-Z]+)(\d)-(\d)$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def expand_reserved(names) -> set[str]:
|
|
20
|
+
"""Expand 'COM1-9' into COM1..COM9."""
|
|
21
|
+
out: set[str] = set()
|
|
22
|
+
for n in names or []:
|
|
23
|
+
m = RESERVED_RANGE.fullmatch(str(n).upper())
|
|
24
|
+
if m:
|
|
25
|
+
stem, lo, hi = m.group(1), int(m.group(2)), int(m.group(3))
|
|
26
|
+
out.update(f"{stem}{i}" for i in range(lo, hi + 1))
|
|
27
|
+
else:
|
|
28
|
+
out.add(str(n).upper())
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_portability(spec, root: Path, path: Path, rel: str) -> list[Finding]:
|
|
33
|
+
"""Cross-platform name and path constraints for one file or directory."""
|
|
34
|
+
p = spec.portability
|
|
35
|
+
findings: list[Finding] = []
|
|
36
|
+
name = path.name
|
|
37
|
+
stem = path.stem
|
|
38
|
+
|
|
39
|
+
forbidden = set(p.get("forbidden_chars") or "") - {"/", "\\", ":"}
|
|
40
|
+
hit = sorted(set(name) & forbidden)
|
|
41
|
+
if hit:
|
|
42
|
+
findings.append(Finding(
|
|
43
|
+
rule="forbidden-char",
|
|
44
|
+
severity=spec.severity("forbidden-char", "error"),
|
|
45
|
+
path=rel,
|
|
46
|
+
message=("name contains characters that are illegal on "
|
|
47
|
+
f"{'/'.join(p['targets'])}: {' '.join(repr(c) for c in hit)}"),
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
if stem.upper() in expand_reserved(p.get("reserved_names")):
|
|
51
|
+
findings.append(Finding(
|
|
52
|
+
rule="reserved-name",
|
|
53
|
+
severity=spec.severity("reserved-name", "error"),
|
|
54
|
+
path=rel,
|
|
55
|
+
message=f"'{stem}' is a reserved device name on Windows and cannot be opened",
|
|
56
|
+
))
|
|
57
|
+
|
|
58
|
+
for ch in p.get("no_trailing") or []:
|
|
59
|
+
if name.endswith(ch):
|
|
60
|
+
findings.append(Finding(
|
|
61
|
+
rule="trailing-char",
|
|
62
|
+
severity=spec.severity("trailing-char", "error"),
|
|
63
|
+
path=rel,
|
|
64
|
+
message=(f"name ends with {ch!r}; Windows strips it silently, so the "
|
|
65
|
+
"name on disk will stop matching the name in your manifest"),
|
|
66
|
+
))
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
max_name = p.get("max_filename")
|
|
70
|
+
if max_name and len(name) > max_name:
|
|
71
|
+
findings.append(Finding(
|
|
72
|
+
rule="filename-too-long",
|
|
73
|
+
severity=spec.severity("filename-too-long", "error"),
|
|
74
|
+
path=rel,
|
|
75
|
+
message=f"filename is {len(name)} characters, limit is {max_name}",
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
max_path = p.get("max_path")
|
|
79
|
+
if max_path and "windows" in p.get("targets", []):
|
|
80
|
+
absolute = len(str(path.resolve()))
|
|
81
|
+
if absolute > max_path:
|
|
82
|
+
findings.append(Finding(
|
|
83
|
+
rule="path-too-long",
|
|
84
|
+
severity=spec.severity("path-too-long", "error"),
|
|
85
|
+
path=rel,
|
|
86
|
+
message=(f"absolute path is {absolute} characters, over the "
|
|
87
|
+
f"{max_path}-character Windows limit"),
|
|
88
|
+
detail={"absolute_length": absolute},
|
|
89
|
+
))
|
|
90
|
+
|
|
91
|
+
if p.get("space_policy") == "forbid" and " " in name:
|
|
92
|
+
findings.append(Finding(
|
|
93
|
+
rule="space-in-name",
|
|
94
|
+
severity=spec.severity("space-in-name", "warning"),
|
|
95
|
+
path=rel,
|
|
96
|
+
message="name contains a space and the spec forbids spaces",
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
if spec.safety.get("unicode") == "nfc" and unicodedata.normalize("NFC", name) != name:
|
|
100
|
+
findings.append(Finding(
|
|
101
|
+
rule="unicode-normalisation",
|
|
102
|
+
severity=spec.severity("unicode-normalisation", "warning"),
|
|
103
|
+
path=rel,
|
|
104
|
+
message=("name is not NFC-normalised; macOS stores decomposed and Windows "
|
|
105
|
+
"composed, so this name compares unequal across platforms"),
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
return findings
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def check_case_collisions(spec, entries: dict[Path, list[str]]) -> list[Finding]:
|
|
112
|
+
"""Sibling names differing only by case break on Windows and macOS."""
|
|
113
|
+
findings: list[Finding] = []
|
|
114
|
+
for parent, names in entries.items():
|
|
115
|
+
buckets: dict[str, list[str]] = {}
|
|
116
|
+
for n in names:
|
|
117
|
+
buckets.setdefault(n.lower(), []).append(n)
|
|
118
|
+
for lowered, group in buckets.items():
|
|
119
|
+
if len(group) > 1:
|
|
120
|
+
findings.append(Finding(
|
|
121
|
+
rule="case-collision",
|
|
122
|
+
severity=spec.severity("case-collision", "error"),
|
|
123
|
+
path=str(parent),
|
|
124
|
+
message=("names differ only by case and cannot coexist on a "
|
|
125
|
+
f"case-insensitive volume: {', '.join(sorted(group))}"),
|
|
126
|
+
detail={"names": sorted(group)},
|
|
127
|
+
))
|
|
128
|
+
return findings
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def check_structure(spec, dirs: list[str], files_by_dir: dict[str, list[str]]) -> list[Finding]:
|
|
132
|
+
"""Required folders present, and no loose files where they are disallowed."""
|
|
133
|
+
findings: list[Finding] = []
|
|
134
|
+
|
|
135
|
+
for pattern, regex in spec.structure.get("_required_re", []):
|
|
136
|
+
if not any(regex.fullmatch(d) for d in dirs):
|
|
137
|
+
findings.append(Finding(
|
|
138
|
+
rule="missing-folder",
|
|
139
|
+
severity=spec.severity("missing-folder", "warning"),
|
|
140
|
+
path=pattern,
|
|
141
|
+
message="no folder in the tree matches this required structure path",
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
for pattern, regex in spec.structure.get("_loose_re", []):
|
|
145
|
+
for d, names in files_by_dir.items():
|
|
146
|
+
if regex.fullmatch(d) and names:
|
|
147
|
+
findings.append(Finding(
|
|
148
|
+
rule="loose-file",
|
|
149
|
+
severity=spec.severity("loose-file", "warning"),
|
|
150
|
+
path=d or ".",
|
|
151
|
+
message=(f"{len(names)} file(s) sit directly in a folder that should "
|
|
152
|
+
f"contain only subfolders: {', '.join(sorted(names)[:4])}"
|
|
153
|
+
+ (" ..." if len(names) > 4 else "")),
|
|
154
|
+
detail={"files": sorted(names)},
|
|
155
|
+
))
|
|
156
|
+
return findings
|
shuttlecheck/cli.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Command line entry point.
|
|
2
|
+
|
|
3
|
+
shuttlecheck ui [folder] open the local app in a browser
|
|
4
|
+
shuttlecheck init <folder> read the convention off an existing tree
|
|
5
|
+
shuttlecheck validate <folder> --spec S report what does not conform
|
|
6
|
+
shuttlecheck verify <folder> check a drive that arrived (no spec needed)
|
|
7
|
+
shuttlecheck check-spec --spec S validate the spec itself
|
|
8
|
+
|
|
9
|
+
None of the above modifies media. That is the whole of this package, and it is
|
|
10
|
+
deliberate: a checker that cannot alter anything is a checker a stranger will
|
|
11
|
+
run on a live job.
|
|
12
|
+
|
|
13
|
+
Installing ShuttleCheck Pro adds the operations that write -- conform, revert,
|
|
14
|
+
history, seal, pack, vendors -- and they appear here as extra subcommands. When
|
|
15
|
+
it is absent those names are still registered, so asking for one gets an
|
|
16
|
+
explanation rather than "invalid choice".
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
from . import extensions
|
|
28
|
+
from .report import Report
|
|
29
|
+
from .sidecar import SidecarError, render_verify, verify
|
|
30
|
+
from .spec import SpecError, load_spec
|
|
31
|
+
from .ui import serve
|
|
32
|
+
from .validate import Validator
|
|
33
|
+
from .version import __version__
|
|
34
|
+
from .walkthrough import present, render_spec_yaml, walkthrough
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
38
|
+
p = argparse.ArgumentParser(
|
|
39
|
+
prog="shuttlecheck",
|
|
40
|
+
description="Naming and delivery conformance for live video and film.")
|
|
41
|
+
p.add_argument("--version", action="version", version=f"shuttlecheck {__version__}")
|
|
42
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
43
|
+
|
|
44
|
+
v = sub.add_parser("validate", help="check a tree and report violations")
|
|
45
|
+
v.add_argument("root")
|
|
46
|
+
v.add_argument("--spec", required=True)
|
|
47
|
+
v.add_argument("--json", action="store_true", help="emit JSON instead of text")
|
|
48
|
+
v.add_argument("--out", help="also write the report to this file")
|
|
49
|
+
v.add_argument("--info", action="store_true", help="include info-level findings")
|
|
50
|
+
v.add_argument("--limit", type=int, default=200,
|
|
51
|
+
help="max findings to print (0 for all)")
|
|
52
|
+
v.add_argument("--strict", action="store_true",
|
|
53
|
+
help="exit non-zero on warnings as well as errors")
|
|
54
|
+
|
|
55
|
+
c = sub.add_parser("check-spec", help="validate the spec itself and exit")
|
|
56
|
+
c.add_argument("--spec", required=True)
|
|
57
|
+
|
|
58
|
+
u = sub.add_parser("ui", help="open the local app in a browser")
|
|
59
|
+
u.add_argument("root", nargs="?", help="folder to open on (optional)")
|
|
60
|
+
u.add_argument("--port", type=int, default=8765)
|
|
61
|
+
u.add_argument("--no-browser", action="store_true",
|
|
62
|
+
help="print the URL instead of opening it")
|
|
63
|
+
|
|
64
|
+
i = sub.add_parser("init", help="read a tree and write the spec it already follows")
|
|
65
|
+
i.add_argument("root")
|
|
66
|
+
i.add_argument("--out", help="default: <root>/_SPEC/spec.yaml")
|
|
67
|
+
i.add_argument("--project-code")
|
|
68
|
+
i.add_argument("--vocabulary", choices=["events", "film"], default="events")
|
|
69
|
+
i.add_argument("--id", dest="spec_id")
|
|
70
|
+
i.add_argument("--yes", action="store_true", help="accept every inference")
|
|
71
|
+
i.add_argument("--force", action="store_true", help="overwrite an existing spec")
|
|
72
|
+
|
|
73
|
+
y = sub.add_parser(
|
|
74
|
+
"verify", help="check a drive against the sidecar it arrived with")
|
|
75
|
+
y.add_argument("root")
|
|
76
|
+
y.add_argument("--hash", action="store_true",
|
|
77
|
+
help="verify contents, not just sizes (slow)")
|
|
78
|
+
y.add_argument("--no-naming", action="store_true",
|
|
79
|
+
help="check arrival only, skip the naming check")
|
|
80
|
+
y.add_argument("--limit", type=int, default=60)
|
|
81
|
+
|
|
82
|
+
pro = extensions.load()
|
|
83
|
+
if pro is not None:
|
|
84
|
+
pro.add_commands(sub)
|
|
85
|
+
else:
|
|
86
|
+
_add_licensed_stubs(sub)
|
|
87
|
+
|
|
88
|
+
return p
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
LICENSED = ("conform", "revert", "history", "seal", "pack", "vendors")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _add_licensed_stubs(sub) -> None:
|
|
95
|
+
"""Register the Pro command names so they explain themselves.
|
|
96
|
+
|
|
97
|
+
argparse would otherwise answer `shuttlecheck conform` with "invalid
|
|
98
|
+
choice", which reads like the command was misspelt rather than not
|
|
99
|
+
installed. These accept anything and fail with a sentence.
|
|
100
|
+
"""
|
|
101
|
+
for name in LICENSED:
|
|
102
|
+
stub = sub.add_parser(name, help="(ShuttleCheck Pro -- not installed)",
|
|
103
|
+
add_help=False)
|
|
104
|
+
stub.add_argument("args", nargs=argparse.REMAINDER)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _licensed_stub(args) -> int:
|
|
108
|
+
try:
|
|
109
|
+
extensions.require(f"`shuttlecheck {args.command}`")
|
|
110
|
+
except extensions.ExtensionMissing as exc:
|
|
111
|
+
print(f"\n{exc}\n", file=sys.stderr)
|
|
112
|
+
return 2
|
|
113
|
+
return 2
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _load(spec_path: str):
|
|
117
|
+
try:
|
|
118
|
+
return load_spec(spec_path)
|
|
119
|
+
except SpecError as exc:
|
|
120
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
121
|
+
raise SystemExit(2)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _require_dir(root: str) -> Path:
|
|
125
|
+
path = Path(root)
|
|
126
|
+
if not path.is_dir():
|
|
127
|
+
print(f"error: not a folder: {path}", file=sys.stderr)
|
|
128
|
+
raise SystemExit(2)
|
|
129
|
+
return path
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def _validate(args) -> int:
|
|
135
|
+
spec = _load(args.spec)
|
|
136
|
+
root = _require_dir(args.root)
|
|
137
|
+
report: Report = Validator(spec, root).run()
|
|
138
|
+
text = (report.to_json() if args.json
|
|
139
|
+
else report.to_text(show_info=args.info, limit=args.limit))
|
|
140
|
+
print(text)
|
|
141
|
+
if args.out:
|
|
142
|
+
Path(args.out).write_text(text, encoding="utf-8")
|
|
143
|
+
if report.errors:
|
|
144
|
+
return 1
|
|
145
|
+
return 1 if args.strict and report.warnings else 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _check_spec(args) -> int:
|
|
149
|
+
spec = _load(args.spec)
|
|
150
|
+
if spec.lint:
|
|
151
|
+
for f in spec.lint:
|
|
152
|
+
print(f"{f.severity}: {f.rule}: {f.message}", file=sys.stderr)
|
|
153
|
+
return 1 if any(f.severity == "error" for f in spec.lint) else 0
|
|
154
|
+
print(f"{args.spec}: valid ({len(spec.tokens)} tokens, {len(spec.contexts)} contexts)")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _ui(args) -> int:
|
|
159
|
+
root = Path(args.root).resolve() if args.root else None
|
|
160
|
+
if root and not root.is_dir():
|
|
161
|
+
print(f"error: not a folder: {root}", file=sys.stderr)
|
|
162
|
+
return 2
|
|
163
|
+
serve(root, port=args.port, open_browser=not args.no_browser)
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _init(args) -> int:
|
|
168
|
+
root = _require_dir(args.root)
|
|
169
|
+
out = Path(args.out) if args.out else root / "_SPEC" / "spec.yaml"
|
|
170
|
+
if out.exists() and not args.force:
|
|
171
|
+
print(f"error: {out} already exists; pass --force to overwrite", file=sys.stderr)
|
|
172
|
+
return 2
|
|
173
|
+
try:
|
|
174
|
+
doc, report, validation = walkthrough(
|
|
175
|
+
root, project_code=args.project_code, vocabulary=args.vocabulary,
|
|
176
|
+
interactive=not args.yes, spec_id=args.spec_id)
|
|
177
|
+
except ValueError as exc:
|
|
178
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
179
|
+
return 2
|
|
180
|
+
|
|
181
|
+
if report is not None:
|
|
182
|
+
if args.yes:
|
|
183
|
+
print(present(report, validation))
|
|
184
|
+
text = render_spec_yaml(doc, report)
|
|
185
|
+
else:
|
|
186
|
+
text = yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100)
|
|
187
|
+
|
|
188
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
out.write_text(text, encoding="utf-8")
|
|
190
|
+
print(f"\n spec written to {out}")
|
|
191
|
+
try:
|
|
192
|
+
load_spec(out)
|
|
193
|
+
except SpecError as exc:
|
|
194
|
+
print(f"\nwarning: the inferred spec does not match the schema:\n{exc}",
|
|
195
|
+
file=sys.stderr)
|
|
196
|
+
return 1
|
|
197
|
+
if report is not None and report.needs_review:
|
|
198
|
+
print(f" {len(report.needs_review)} item(s) marked for review in the header.")
|
|
199
|
+
print(f"\n next: shuttlecheck validate {root} --spec {out}")
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _verify(args) -> int:
|
|
204
|
+
root = _require_dir(args.root)
|
|
205
|
+
try:
|
|
206
|
+
result = verify(root, check_hashes=args.hash,
|
|
207
|
+
check_naming=not args.no_naming)
|
|
208
|
+
except SidecarError as exc:
|
|
209
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
210
|
+
return 2
|
|
211
|
+
print(render_verify(result, limit=args.limit))
|
|
212
|
+
if not result.intact:
|
|
213
|
+
return 1
|
|
214
|
+
return 1 if result.naming is not None and result.naming.errors else 0
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
COMMANDS = {"ui": _ui, "validate": _validate, "check-spec": _check_spec,
|
|
218
|
+
"init": _init, "verify": _verify}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _handlers() -> dict:
|
|
222
|
+
handlers = dict(COMMANDS)
|
|
223
|
+
pro = extensions.load()
|
|
224
|
+
if pro is not None:
|
|
225
|
+
handlers.update(pro.COMMANDS)
|
|
226
|
+
else:
|
|
227
|
+
handlers.update({name: _licensed_stub for name in LICENSED})
|
|
228
|
+
return handlers
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def main(argv=None) -> int:
|
|
232
|
+
args = build_parser().parse_args(argv)
|
|
233
|
+
try:
|
|
234
|
+
return _handlers()[args.command](args)
|
|
235
|
+
except SystemExit as exc:
|
|
236
|
+
return int(exc.code or 0)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
raise SystemExit(main())
|