shuttlecheck 0.4.0__tar.gz → 0.4.2__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.
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/PKG-INFO +1 -1
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/pyproject.toml +2 -2
- shuttlecheck-0.4.2/shuttlecheck/certificate.py +110 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/checks.py +1 -1
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/cli.py +9 -2
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/completeness.py +44 -5
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/conventions.py +20 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/delivery.py +4 -1
- shuttlecheck-0.4.2/shuttlecheck/demo.py +160 -0
- shuttlecheck-0.4.2/shuttlecheck/demo_convention.yaml +411 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/jobs.py +17 -1
- shuttlecheck-0.4.2/shuttlecheck/layoutlog.py +66 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/mhl.py +1 -1
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/prefolder.py +82 -2
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/provenance.py +5 -1
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/say.py +17 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/seal.py +24 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/seal_api.py +74 -5
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/sidecar.py +1 -1
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/today.py +56 -6
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui.py +222 -4
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/app.css +23 -23
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/app.html +1 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/app.js +455 -94
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/validate.py +1 -1
- shuttlecheck-0.4.2/shuttlecheck/version.py +1 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/PKG-INFO +1 -1
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/SOURCES.txt +4 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_jobs.py +1 -1
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_prefolder.py +42 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_the_04_window.py +125 -3
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_window.py +41 -0
- shuttlecheck-0.4.0/shuttlecheck/version.py +0 -1
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/LICENSE +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/PYPI.md +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/README.md +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/setup.cfg +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/__init__.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/admission.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ale.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/aliases.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/arrivals.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/attribution.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/auth.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/endpoints.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/extensions.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/infer.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/journal.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/listener.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/marks.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/originals.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/patterns.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/probe.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/receive.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/report.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/schedule.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/schema.json +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/sizing.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/spec.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/store.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/support.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/telemetry-v1.schema.json +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/telemetry.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/templates.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/transport.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/classic.css +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/classic.html +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/classic.js +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/display-LICENSE.txt +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/display.woff2 +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/inter-LICENSE.txt +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/inter.woff2 +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/mono-LICENSE.txt +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/mono.woff2 +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/updates.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/walkthrough.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck/window.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/dependency_links.txt +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/entry_points.txt +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/requires.txt +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/top_level.txt +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_admission.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_ale.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_aliases.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_allocate.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_arrivals.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_auth.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_auth_packaged.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_check_against.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_classify.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_cli.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_completeness.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_consoles.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_conventions.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_conventions_routes.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_counting_files.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_dailies.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_dailiesdrop.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_delivery.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_device_flow.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_due.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_endpoints.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_enrolment.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_fleet_units.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_folders.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_fonts.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_groups.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_infer.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_infer_keys.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_keepable.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_lamps.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_listener.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_marks_api.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_message_style.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_meters_measure.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_mhl.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_min_version.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_monitor.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_no_convention.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_one_file_is_not_a_pattern.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_originals.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_path_length.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_pick.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_pointed_at_the_job.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_problems.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_provenance.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_rack.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_receive.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_rename_token.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_report.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_reveal.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_rollcall_schedule.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_signal_path.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_sizing.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_sizing_history.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_source_attach.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_store.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_support.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_sweeping.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_telemetry.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_templates.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_the_analyser.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_the_pipeline.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_today.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_today_screen.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_trademark.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_transport.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_ui.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_ui_routes.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_unread_sources.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_updates.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_validate.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_verify.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_walk.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_web_seal.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_what_the_far_end_reads.py +0 -0
- {shuttlecheck-0.4.0 → shuttlecheck-0.4.2}/tests/test_where_it_belongs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shuttlecheck
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Check media filenames, folder structure and deliverables against a written convention. Read-only.
|
|
5
5
|
Author-email: MVRK Systems <hello@mvrk.systems>
|
|
6
6
|
License: Proprietary — free to use, no redistribution
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "shuttlecheck"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2"
|
|
4
4
|
description = "Check media filenames, folder structure and deliverables against a written convention. Read-only."
|
|
5
5
|
readme = { file = "PYPI.md", content-type = "text/markdown" }
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -71,4 +71,4 @@ packages = ["shuttlecheck"]
|
|
|
71
71
|
# it is sent, so a build that cannot read it sends nothing at all. Both travel
|
|
72
72
|
# inside the wheel.
|
|
73
73
|
shuttlecheck = ["schema.json", "telemetry-v1.schema.json", "ui_assets/*",
|
|
74
|
-
"ui_assets/fonts/*"]
|
|
74
|
+
"ui_assets/fonts/*", "demo_convention.yaml"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""The certificate a sealed drive carries.
|
|
2
|
+
|
|
3
|
+
`_SPEC/README.txt` is for somebody who has never heard of any of this and
|
|
4
|
+
opens a text file. `_SPEC/SEALED.html` is the same facts for everybody else:
|
|
5
|
+
the page a post house double-clicks first. It opens in any browser, offline,
|
|
6
|
+
with nothing fetched, and says what was sealed, against which rule, in what
|
|
7
|
+
state, and how to prove it arrived intact without installing anything of ours.
|
|
8
|
+
|
|
9
|
+
It is also the only thing this product puts in front of somebody who is not
|
|
10
|
+
yet a customer, on a drive their own client sent them. So it is plain, exact
|
|
11
|
+
and quietly made, and it does not sell.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import html
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _esc(value) -> str:
|
|
20
|
+
return html.escape(str(value if value is not None else ""), quote=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render(*, folder: str, project: str, convention: str, sealed: str,
|
|
24
|
+
mhl_name: str, generation: int, algorithm: str, files: int,
|
|
25
|
+
size: str, errors, warnings, custody: str = "",
|
|
26
|
+
tool_version: str = "") -> str:
|
|
27
|
+
# `None` for both is a preview drawn before sealing: the conformance is
|
|
28
|
+
# measured when the drive is sealed, and is never shown as clean before.
|
|
29
|
+
preview = errors is None or warnings is None
|
|
30
|
+
clean = not preview and not errors and not warnings
|
|
31
|
+
state = ("Measured when the drive is sealed" if preview else
|
|
32
|
+
"On the rule" if clean else
|
|
33
|
+
f"{errors} error{'s' if errors != 1 else ''}, "
|
|
34
|
+
f"{warnings} warning{'s' if warnings != 1 else ''} when sealed")
|
|
35
|
+
ring = (f"ASC MHL · {algorithm.upper()} · {files} FILES · GENERATION "
|
|
36
|
+
f"{generation} · ")
|
|
37
|
+
rows = [
|
|
38
|
+
("Drive", folder), ("Project", project), ("Convention", convention),
|
|
39
|
+
("Sealed", sealed), ("Hash list", f"ascmhl/{mhl_name}"),
|
|
40
|
+
("Generation", generation), ("Checksum", algorithm),
|
|
41
|
+
("Files", files), ("Size", size), ("Conformance", state),
|
|
42
|
+
]
|
|
43
|
+
if custody:
|
|
44
|
+
rows.append(("Custody", custody))
|
|
45
|
+
table = "\n".join(
|
|
46
|
+
f"<tr><th>{_esc(k)}</th><td>{_esc(v)}</td></tr>" for k, v in rows)
|
|
47
|
+
return f"""<!DOCTYPE html>
|
|
48
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
50
|
+
<title>{'Not sealed yet' if preview else 'Sealed'}: {_esc(folder)}</title>
|
|
51
|
+
<style>
|
|
52
|
+
:root {{ --bg:#07142A; --panel:#0C1D38; --line:rgba(130,175,225,.2); --fg:#EAF2FB;
|
|
53
|
+
--fg2:#A9BCD3; --cyan:#34E7E3; --ok:#2FBF71; --warn:#FF9A5C; color-scheme: dark; }}
|
|
54
|
+
* {{ box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact; }}
|
|
55
|
+
html {{ background: var(--bg); }}
|
|
56
|
+
body {{ margin:0; background:var(--bg); color:var(--fg);
|
|
57
|
+
font:15px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; }}
|
|
58
|
+
main {{ max-width:880px; margin:0 auto; padding:48px 20px 64px; display:grid; gap:28px; }}
|
|
59
|
+
.top {{ display:grid; grid-template-columns:220px 1fr; gap:28px; align-items:center; }}
|
|
60
|
+
h1 {{ margin:0; font-size:40px; line-height:1.05; letter-spacing:-.5px; }}
|
|
61
|
+
.sub {{ color:var(--fg2); margin:8px 0 0; }}
|
|
62
|
+
.state {{ display:inline-block; margin-top:14px; padding:4px 12px; border-radius:999px;
|
|
63
|
+
border:1px solid currentColor; font-weight:600; font-size:13px;
|
|
64
|
+
color:{'var(--fg2)' if preview else 'var(--ok)' if clean else 'var(--warn)'}; }}
|
|
65
|
+
table {{ width:100%; border-collapse:collapse; background:var(--panel);
|
|
66
|
+
border:1px solid var(--line); border-radius:12px; overflow:hidden; }}
|
|
67
|
+
th, td {{ text-align:left; padding:10px 16px; border-top:1px solid var(--line); vertical-align:top; }}
|
|
68
|
+
tr:first-child th, tr:first-child td {{ border-top:0; }}
|
|
69
|
+
th {{ width:160px; color:var(--fg2); font-weight:500; }}
|
|
70
|
+
td {{ font-family:ui-monospace,Menlo,Consolas,monospace; font-size:13.5px; word-break:break-all; }}
|
|
71
|
+
h2 {{ font-size:18px; margin:0 0 8px; }}
|
|
72
|
+
p {{ margin:0 0 10px; color:var(--fg2); max-width:68ch; }}
|
|
73
|
+
code {{ font-family:ui-monospace,Menlo,Consolas,monospace; color:var(--fg); }}
|
|
74
|
+
.ribbon {{ display:inline-block; margin:0 0 12px; padding:3px 10px; border-radius:6px;
|
|
75
|
+
background:var(--warn); color:#07142A; font-weight:700; font-size:12px; letter-spacing:.04em; text-transform:uppercase; }}
|
|
76
|
+
footer {{ color:var(--fg2); font-size:13px; border-top:1px solid var(--line); padding-top:16px; }}
|
|
77
|
+
@media (max-width:640px) {{ .top {{ grid-template-columns:1fr; }} }}
|
|
78
|
+
/* Dark on paper too: the certificate is the brand a client's post house
|
|
79
|
+
sees, and a browser printing it drops backgrounds unless told not to. */
|
|
80
|
+
@page {{ margin: 0; background: #07142A; }}
|
|
81
|
+
@media print {{ html, body {{ background: var(--bg) !important; }} main {{ padding: 28px 32px; }}
|
|
82
|
+
table, section {{ break-inside: avoid; }} }}
|
|
83
|
+
</style></head>
|
|
84
|
+
<body><main>
|
|
85
|
+
<div class="top">
|
|
86
|
+
<svg viewBox="0 0 220 220" width="200" height="200" role="img" aria-label="Seal, generation {generation}">
|
|
87
|
+
<defs><path id="r" d="M110 110 m -76 0 a 76 76 0 1 1 152 0 a 76 76 0 1 1 -152 0"/>
|
|
88
|
+
<linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#0B7AC1"/>
|
|
89
|
+
<stop offset=".6" stop-color="#19A3C8"/><stop offset="1" stop-color="#34E7E3"/></linearGradient></defs>
|
|
90
|
+
<circle cx="110" cy="110" r="96" fill="none" stroke="url(#g)" stroke-width="9"/>
|
|
91
|
+
<circle cx="110" cy="110" r="60" fill="none" stroke="var(--line)"/>
|
|
92
|
+
<text font-size="9.5" fill="var(--fg2)" font-family="ui-monospace,Menlo,monospace"><textPath href="#r" textLength="470" lengthAdjust="spacing">{_esc(ring)}</textPath></text>
|
|
93
|
+
<text x="110" y="102" text-anchor="middle" font-size="11" fill="var(--fg2)">generation</text>
|
|
94
|
+
<text x="110" y="140" text-anchor="middle" font-size="40" font-weight="700" fill="var(--fg)">{_esc(generation)}</text>
|
|
95
|
+
</svg>
|
|
96
|
+
<div>{'<p class="ribbon">Preview. Nothing is sealed yet.</p>' if preview else ''}<h1>{'Ready to seal' if preview else 'Sealed'}: {_esc(folder)}</h1>
|
|
97
|
+
<p class="sub">{'When this drive is sealed, every file on it is hashed into an ASC Media Hash List, and the naming convention it is held to travels with it.' if preview else 'Every file on this drive was hashed into an ASC Media Hash List when it was sealed, and the naming convention it was held to travels with it.'}</p>
|
|
98
|
+
<span class="state">{_esc(state)}</span></div>
|
|
99
|
+
</div>
|
|
100
|
+
<table>{table}</table>
|
|
101
|
+
<section><h2>Prove it arrived intact</h2>
|
|
102
|
+
<p>Use whatever already reads ASC MHL: Pomfort MediaVerify, or the ASC's own <code>ascmhl</code> tool. The hash list in <code>ascmhl/</code> is plain XML and opens in any text editor.</p>
|
|
103
|
+
<p>Or, with ShuttleCheck, free and with no account: <code>pip install shuttlecheck</code>, then <code>shuttlecheck verify .</code> from the folder holding <code>_SPEC</code>. It only reads; nothing is renamed, moved or deleted.</p>
|
|
104
|
+
</section>
|
|
105
|
+
<section><h2>What else is here</h2>
|
|
106
|
+
<p><code>_SPEC/convention.yaml</code> is the naming convention this job follows, <code>_SPEC/report.txt</code> the conformance report as it stood when the drive left, and <code>_SPEC/README.txt</code> the same as this page in plain text.</p>
|
|
107
|
+
</section>
|
|
108
|
+
<footer>Sealed with ShuttleCheck{(' ' + _esc(tool_version)) if tool_version else ''} by MVRK Systems, mvrk.systems. Nothing on this page was fetched from anywhere.</footer>
|
|
109
|
+
</main></body></html>
|
|
110
|
+
"""
|
|
@@ -169,7 +169,7 @@ def check_structure(spec, dirs: list[str], files_by_dir: dict[str, list[str]],
|
|
|
169
169
|
rule="loose-file",
|
|
170
170
|
severity=spec.severity("loose-file", "warning"),
|
|
171
171
|
path=d or ".",
|
|
172
|
-
message=(f"{count(len(names), 'file')} sit directly in a folder that should "
|
|
172
|
+
message=(f"{count(len(names), 'file')} {'sits' if len(names) == 1 else 'sit'} directly in a folder that should "
|
|
173
173
|
f"contain only subfolders: {', '.join(sorted(names)[:4])}"
|
|
174
174
|
+ (" ..." if len(names) > 4 else "")),
|
|
175
175
|
detail={"files": sorted(names)},
|
|
@@ -69,11 +69,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
69
69
|
b.add_argument("--yes", action="store_true",
|
|
70
70
|
help="write it without printing the contents first")
|
|
71
71
|
|
|
72
|
-
u = sub.add_parser("ui", help="open the local app in a browser")
|
|
72
|
+
u = sub.add_parser("ui", help="open the local app in a browser, or in its own window")
|
|
73
73
|
u.add_argument("root", nargs="?", help="folder to open on (optional)")
|
|
74
74
|
u.add_argument("--port", type=int, default=8765)
|
|
75
75
|
u.add_argument("--no-browser", action="store_true",
|
|
76
76
|
help="print the URL instead of opening it")
|
|
77
|
+
# What the packaged app does, available from a checkout. Without it a
|
|
78
|
+
# checkout can only ever show the tool as a tab, and the window is the one
|
|
79
|
+
# part of the product nobody sees until a build ships.
|
|
80
|
+
u.add_argument("--window", action="store_true",
|
|
81
|
+
help="open in its own window (needs the [app] extra); "
|
|
82
|
+
"falls back to a browser where there is no webview")
|
|
77
83
|
|
|
78
84
|
i = sub.add_parser("init", help="read a tree and write the spec it already follows")
|
|
79
85
|
i.add_argument("root")
|
|
@@ -187,7 +193,8 @@ def _ui(args) -> int:
|
|
|
187
193
|
if root and not root.is_dir():
|
|
188
194
|
print(f"error: not a folder: {root}", file=sys.stderr)
|
|
189
195
|
return 2
|
|
190
|
-
serve(root, port=args.port, open_browser=not args.no_browser
|
|
196
|
+
serve(root, port=args.port, open_browser=not args.no_browser,
|
|
197
|
+
shell=args.window and not args.no_browser)
|
|
191
198
|
return 0
|
|
192
199
|
|
|
193
200
|
|
|
@@ -110,10 +110,15 @@ class Ledger:
|
|
|
110
110
|
weighed: dict = field(default_factory=dict)
|
|
111
111
|
missing: list = field(default_factory=list) # Declared, nothing arrived
|
|
112
112
|
undeclared: list = field(default_factory=list) # arrived, nothing declared
|
|
113
|
+
# Declared, and here under a name the rule could not read. Not missing:
|
|
114
|
+
# telling a camera crew they never delivered a session that is sitting
|
|
115
|
+
# on the drive with a typo in it is the one mistake this screen must not
|
|
116
|
+
# make. key -> the files that name it.
|
|
117
|
+
misnamed: dict = field(default_factory=dict)
|
|
113
118
|
|
|
114
119
|
@property
|
|
115
120
|
def complete(self) -> bool:
|
|
116
|
-
return not self.missing
|
|
121
|
+
return not self.missing and not self.misnamed
|
|
117
122
|
|
|
118
123
|
@property
|
|
119
124
|
def arrived(self) -> int:
|
|
@@ -317,6 +322,7 @@ class Ledger:
|
|
|
317
322
|
# job, only its holes.
|
|
318
323
|
"roster": [{"label": d.label, "fields": d.fields, "row": d.row,
|
|
319
324
|
"files": self.delivered.get(d.key, 0),
|
|
325
|
+
"misnamed": len(self.misnamed.get(d.key, ())),
|
|
320
326
|
"who": self.whose(d),
|
|
321
327
|
# Whether the job has got this far. A row nothing
|
|
322
328
|
# arrived for reads as a hole or as work still to
|
|
@@ -338,6 +344,10 @@ class Ledger:
|
|
|
338
344
|
"where": where_it_belongs(d, self.folders,
|
|
339
345
|
self.tokens, self.settled)}
|
|
340
346
|
for d in self.missing],
|
|
347
|
+
"misnamed": [{"label": d.label, "fields": d.fields, "row": d.row,
|
|
348
|
+
"who": self.whose(d),
|
|
349
|
+
"paths": self.misnamed[d.key][:5]}
|
|
350
|
+
for d in self.declared if d.key in self.misnamed],
|
|
341
351
|
"undeclared": [{"label": " ".join(f"{k}={v}" for k, v in dict(
|
|
342
352
|
zip(self.key_tokens, key)).items()), "files": self.delivered[key]}
|
|
343
353
|
for key in self.undeclared],
|
|
@@ -660,16 +670,44 @@ def reconcile(declared: list[Declared], records, key_tokens: list,
|
|
|
660
670
|
seen["rows"].add(key)
|
|
661
671
|
|
|
662
672
|
declared_keys = {d.key for d in declared}
|
|
673
|
+
unread = [r for r in records
|
|
674
|
+
if not all(t in r.tokens for t in key_tokens)]
|
|
663
675
|
for entry in declared:
|
|
664
676
|
if not ledger.delivered.get(entry.key):
|
|
665
677
|
ledger.delivered.setdefault(entry.key, 0)
|
|
666
|
-
|
|
678
|
+
named = _named_anyway(entry, unread, key_tokens)
|
|
679
|
+
if named:
|
|
680
|
+
ledger.misnamed[entry.key] = named
|
|
681
|
+
else:
|
|
682
|
+
ledger.missing.append(entry)
|
|
667
683
|
|
|
668
684
|
ledger.undeclared = sorted(k for k, n in ledger.delivered.items()
|
|
669
685
|
if n and k not in declared_keys)
|
|
670
686
|
return ledger
|
|
671
687
|
|
|
672
688
|
|
|
689
|
+
def _named_anyway(entry: "Declared", records, key_tokens: list) -> list[str]:
|
|
690
|
+
"""Files the rule could not read that still name this declared row.
|
|
691
|
+
|
|
692
|
+
Conservative on purpose: every key value must appear in the path as a
|
|
693
|
+
whole word (`S003`, `D01`, `EXC26`), or have been read from it already.
|
|
694
|
+
`MAINSTAGE_S003` counts for S003; `S0031` does not, and a title alone
|
|
695
|
+
never does, because a near miss here would hide a real hole.
|
|
696
|
+
"""
|
|
697
|
+
import re as _re
|
|
698
|
+
|
|
699
|
+
wanted = [str(entry.fields.get(t, "")).lower() for t in key_tokens]
|
|
700
|
+
if not all(wanted):
|
|
701
|
+
return []
|
|
702
|
+
out = []
|
|
703
|
+
for record in records:
|
|
704
|
+
words = set(w.lower() for w in _re.split(r"[^A-Za-z0-9]+", str(record.rel or "")) if w)
|
|
705
|
+
have = {str(v).lower() for v in (record.tokens or {}).values()}
|
|
706
|
+
if all(v in words or v in have for v in wanted):
|
|
707
|
+
out.append(str(record.rel))
|
|
708
|
+
return out
|
|
709
|
+
|
|
710
|
+
|
|
673
711
|
def findings(ledger: Ledger, severities) -> list[Finding]:
|
|
674
712
|
"""A missing reel is an error in the report, not a note on a side screen.
|
|
675
713
|
|
|
@@ -693,7 +731,8 @@ def findings(ledger: Ledger, severities) -> list[Finding]:
|
|
|
693
731
|
rule="declared-never-delivered",
|
|
694
732
|
severity=severities("declared-never-delivered", "error"),
|
|
695
733
|
path=entry.label or "(unnamed)", context=ledger.source,
|
|
696
|
-
message=(f"{
|
|
734
|
+
message=(f"{' '.join(str(v) for v in entry.fields.values()) or entry.label} "
|
|
735
|
+
f"is on {'the running order' if ledger.source == 'schedule' else (ledger.source or 'the manifest')} "
|
|
697
736
|
f"(row {entry.row}) and nothing was ever delivered for it"),
|
|
698
737
|
detail={"source": ledger.source, "row": entry.row,
|
|
699
738
|
"fields": dict(entry.fields)},
|
|
@@ -704,8 +743,8 @@ def findings(ledger: Ledger, severities) -> list[Finding]:
|
|
|
704
743
|
rule="delivered-never-declared",
|
|
705
744
|
severity=severities("delivered-never-declared", "warning"),
|
|
706
745
|
path=described, context=ledger.source,
|
|
707
|
-
message=(f"{
|
|
708
|
-
f"{ledger.source or 'the manifest'}"),
|
|
746
|
+
message=(f"{' '.join(str(v) for v in key)} was delivered but is not on "
|
|
747
|
+
f"{'the running order' if ledger.source == 'schedule' else (ledger.source or 'the manifest')}"),
|
|
709
748
|
detail={"source": ledger.source, "files": ledger.delivered[key]},
|
|
710
749
|
))
|
|
711
750
|
return out
|
|
@@ -32,6 +32,7 @@ from __future__ import annotations
|
|
|
32
32
|
from .say import count
|
|
33
33
|
|
|
34
34
|
import copy
|
|
35
|
+
import json
|
|
35
36
|
import re
|
|
36
37
|
import uuid
|
|
37
38
|
from datetime import datetime, timezone
|
|
@@ -512,6 +513,25 @@ def summaries() -> list[dict]:
|
|
|
512
513
|
} for r in all_of_them()]
|
|
513
514
|
|
|
514
515
|
|
|
516
|
+
def identical(name: str, doc: dict) -> dict | None:
|
|
517
|
+
"""A convention already kept under this name with the same rules, if any.
|
|
518
|
+
|
|
519
|
+
Compared without the revision and the date, which are what saving
|
|
520
|
+
changes, so the same document kept twice is recognised as the same.
|
|
521
|
+
"""
|
|
522
|
+
def bare(d):
|
|
523
|
+
d = dict(d or {})
|
|
524
|
+
d.pop("revision", None)
|
|
525
|
+
d.pop("updated", None)
|
|
526
|
+
return json.dumps(d, sort_keys=True, default=str)
|
|
527
|
+
|
|
528
|
+
wanted = (str(name or "").strip()[:MAX_NAME] or "Untitled convention", bare(doc))
|
|
529
|
+
for row in all_of_them():
|
|
530
|
+
if (row.get("name"), bare(row.get("doc"))) == wanted:
|
|
531
|
+
return row
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
|
|
515
535
|
def get(cid: str) -> dict | None:
|
|
516
536
|
return store._read_json(_file(cid), None)
|
|
517
537
|
|
|
@@ -605,7 +605,10 @@ def check_packages(packages: dict, records: list[FileRecord],
|
|
|
605
605
|
rule="incomplete-package",
|
|
606
606
|
severity=_shortfall_severity(requirement, severities),
|
|
607
607
|
path=described or name, context=name,
|
|
608
|
-
|
|
608
|
+
# Said the way a person says it: "Session package EXC26
|
|
609
|
+
# D01 S005", not "project=EXC26 day=D01 session=S005".
|
|
610
|
+
message=_shortfall(label, " ".join(str(v) for v in key),
|
|
611
|
+
context, count, rule),
|
|
609
612
|
detail={"package": name, "key": described,
|
|
610
613
|
"context": context, "found": count,
|
|
611
614
|
"wanted": _describe(rule)},
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""A sample drive, for somebody who has not got one to hand.
|
|
2
|
+
|
|
3
|
+
A conference: two days, four rooms, a keyed running order, nine suppliers
|
|
4
|
+
on six roads, some of the job delivered, some of it late, and a handful of
|
|
5
|
+
names off the rule. Every tool has something true to show on it. The files
|
|
6
|
+
are zeros, not media, and the drive says so in its own name.
|
|
7
|
+
|
|
8
|
+
Built into the app's own folder, never anywhere somebody chose, so trying
|
|
9
|
+
the product writes nothing near anybody's work.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from zoneinfo import ZoneInfo
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
EXAMPLE = Path(__file__).resolve().parent / "demo_convention.yaml"
|
|
23
|
+
TZ = ZoneInfo("Europe/Malta")
|
|
24
|
+
|
|
25
|
+
AGENDA = """\
|
|
26
|
+
Session ID,Session Title,Room,Day,Start,End,Speakers,Sponsor
|
|
27
|
+
S001,Opening Keynote,Main Stage,1,09:00,10:00,Jane Doe;Ravi Patel,
|
|
28
|
+
S002,Scaling Payments,Tech Stage,1,10:30,11:15,Marta Kowalski,Acme Corp
|
|
29
|
+
S003,Regulation Panel,Main Stage,1,11:30,12:30,Jane Doe,
|
|
30
|
+
S004,AI in Compliance,Tech Stage,1,13:00,13:45,Sam Okoro,
|
|
31
|
+
S005,Colour Pipelines,Workshop Room,1,10:00,11:00,Elena Nowak,
|
|
32
|
+
S006,Founder Interviews,Interview Studio,1,12:00,13:00,Leo Marsh,Northwind
|
|
33
|
+
S007,Closing Remarks,Main Stage,1,16:00,16:30,Jane Doe,
|
|
34
|
+
S011,Day Two Keynote,Main Stage,2,09:00,10:00,Ravi Patel,Northwind
|
|
35
|
+
S012,Payments at the Edge,Tech Stage,2,10:30,11:15,Marta Kowalski,Acme Corp
|
|
36
|
+
S013,Audio for Streams,Workshop Room,2,10:00,11:00,Felix Silva,
|
|
37
|
+
S014,Investor Panel,Main Stage,2,11:30,12:30,Dana Okafor,
|
|
38
|
+
S015,Creator Interviews,Interview Studio,2,12:00,13:00,Leo Marsh,
|
|
39
|
+
S016,Security Deep Dive,Tech Stage,2,13:30,14:15,Sam Okoro,
|
|
40
|
+
S017,Awards,Main Stage,2,17:00,18:00,Jane Doe,Acme Corp
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
VENDORS = {
|
|
44
|
+
"crew_main": {"label": "Hall 1 camera crew", "contact": "hall1@lisbonbroadcast.example",
|
|
45
|
+
"owns": ["iso_record"], "where": {"stage": "MAIN"},
|
|
46
|
+
"via": "dailiesdrop", "unit": "DD-PRO-0117"},
|
|
47
|
+
"crew_tech": {"label": "Hall 2 camera crew", "contact": "hall2@lisbonbroadcast.example",
|
|
48
|
+
"owns": ["iso_record"], "where": {"stage": "TECH"},
|
|
49
|
+
"via": "dailiesdrop", "unit": "DD-PRO-0117"},
|
|
50
|
+
"crew_wksp": {"label": "Workshop crew", "contact": "wksp@lisbonbroadcast.example",
|
|
51
|
+
"owns": ["iso_record"], "where": {"stage": "WKSP"},
|
|
52
|
+
"via": "dailiesdrop", "unit": "DD-PRO-0122"},
|
|
53
|
+
"crew_stdo": {"label": "Studio crew", "contact": "studio@lisbonbroadcast.example",
|
|
54
|
+
"owns": ["iso_record"], "where": {"stage": "STDO"},
|
|
55
|
+
"via": "shuttle"},
|
|
56
|
+
"master_edit": {"label": "Tejo Post", "contact": "deliveries@tejopost.example",
|
|
57
|
+
"due": "2026-11-13", "owns": ["session_master"], "via": "dailiesdrop",
|
|
58
|
+
"unit": "DD-PRO-0122"},
|
|
59
|
+
"social_edit": {"label": "Northlight Edit", "contact": "deliveries@northlight.example",
|
|
60
|
+
"due": "2026-11-14", "owns": ["social_cutdown"], "via": "frameio"},
|
|
61
|
+
"sponsor_agency": {"label": "Meridian Sponsorship", "contact": "post@meridian.example",
|
|
62
|
+
"due": "2026-11-18", "owns": ["sponsor_deliverable"],
|
|
63
|
+
"via": "wetransfer"},
|
|
64
|
+
"captions": {"label": "Verbatim Captioning", "contact": "ops@verbatim.example",
|
|
65
|
+
"due": "2026-11-16", "owns": ["speaker_deliverable"], "via": "gdrive"},
|
|
66
|
+
"proxy_house": {"label": "Lisbon Proxy", "contact": "ops@lisbonproxy.example",
|
|
67
|
+
"owns": ["proxy"], "via": "aspera"},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
M = "EXC26/D0{d}/{st}/MASTERS/EXC26_D0{d}_{st}_{s}_{t}_v01.mov"
|
|
71
|
+
|
|
72
|
+
FILES = [
|
|
73
|
+
# Day one masters, mostly right
|
|
74
|
+
(M.format(d=1, st="MAIN", s="S001", t="opening-keynote"), 1, "09:05"),
|
|
75
|
+
(M.format(d=1, st="TECH", s="S002", t="scaling-payments"), 1, "10:35"),
|
|
76
|
+
(M.format(d=1, st="WKSP", s="S005", t="colour-pipelines"), 1, "10:05"),
|
|
77
|
+
(M.format(d=1, st="STDO", s="S006", t="founder-interviews"), 1, "12:05"),
|
|
78
|
+
(M.format(d=1, st="MAIN", s="S007", t="closing-remarks"), 1, "16:05"),
|
|
79
|
+
# Off the rule
|
|
80
|
+
("EXC26/D01/MAIN/MASTERS/EXC26_D01_MAINSTAGE_S003_regulation-panel_v01.mov", 1, "11:35"),
|
|
81
|
+
("EXC26/D01/TECH/MASTERS/EXC26_D01_TECH_S004_ai-in-compliance.mov", 1, "13:05"),
|
|
82
|
+
("EXC26/D01/MAIN/MASTERS/exc26 d01 main s007 closing remarks FINAL.mov", 1, "16:10"),
|
|
83
|
+
("EXC26/D01/MAIN/MASTERS/EXC26_D01_MAIN_S009_overnight-test_v01.mov", 1, "03:00"),
|
|
84
|
+
("EXC26/D01/handover-notes.txt", None, None),
|
|
85
|
+
# Day two, half in
|
|
86
|
+
(M.format(d=2, st="MAIN", s="S011", t="day-two-keynote"), 2, "09:05"),
|
|
87
|
+
(M.format(d=2, st="TECH", s="S012", t="payments-at-the-edge"), 2, "10:35"),
|
|
88
|
+
(M.format(d=2, st="WKSP", s="S013", t="audio-for-streams"), 2, "10:05"),
|
|
89
|
+
("EXC26/D02/MAIN/MASTERS/Investor Panel MASTER.mov", 2, "11:35"),
|
|
90
|
+
# Recorder originals, never renamed
|
|
91
|
+
("EXC26/D01/MAIN/ISO/CAM01/A001C001_261110_ABCD.mov", 1, "09:05"),
|
|
92
|
+
("EXC26/D01/MAIN/ISO/CAM02/A002C001_261110_ABCD.mov", 1, "09:05"),
|
|
93
|
+
("EXC26/D01/TECH/ISO/CAM01/B001C004_261110_EF01.mov", 1, "10:35"),
|
|
94
|
+
("EXC26/D01/WKSP/ISO/CAM01/C001C002_261110_7A21.mov", 1, "10:05"),
|
|
95
|
+
("EXC26/D02/MAIN/ISO/CAM01/A001C031_261111_ABCD.mov", 2, "09:05"),
|
|
96
|
+
("EXC26/D02/TECH/ISO/CAM01/B001C022_261111_EF01.mov", 2, "10:35"),
|
|
97
|
+
# Proxies
|
|
98
|
+
("EXC26/PROXY/D01/MAIN/EXC26_D01_MAIN_S001_CAM01_proxy.mp4", None, None),
|
|
99
|
+
("EXC26/PROXY/D01/TECH/EXC26_D01_TECH_S002_CAM01_proxy.mp4", None, None),
|
|
100
|
+
("EXC26/PROXY/D01/MAIN/S003 cam1 proxy.mp4", None, None),
|
|
101
|
+
# Deliverables
|
|
102
|
+
("EXC26/DELIVERABLES/SPEAKERS/D01/EXC26_D01_S001_doe-jane_opening-keynote_v01.mp4", None, None),
|
|
103
|
+
("EXC26/DELIVERABLES/SPEAKERS/D01/EXC26_D01_S002_kowalski-marta_scaling-payments_v01.mp4", None, None),
|
|
104
|
+
("EXC26/DELIVERABLES/SPONSORS/acme-corp/EXC26_acme-corp_D01_S002_scaling-payments_16x9_v01.mp4", None, None),
|
|
105
|
+
("EXC26/DELIVERABLES/SPONSORS/northwind/northwind cut v2.mp4", None, None),
|
|
106
|
+
("EXC26/SOCIAL/D01/EXC26_S001_opening-keynote_9x16_v01.mp4", None, None),
|
|
107
|
+
("EXC26/SOCIAL/D01/EXC26_S002_scaling-payments_1x1_v01.mp4", None, None),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build(root: Path) -> Path:
|
|
112
|
+
"""Lay the sample out under `root`; returns the convention it carries."""
|
|
113
|
+
root = Path(root)
|
|
114
|
+
spec_dir = root / "EXC26" / "_SPEC"
|
|
115
|
+
spec_dir.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
doc = yaml.safe_load(EXAMPLE.read_text(encoding="utf-8"))
|
|
118
|
+
# The files are zeros, not media: delivery rules would only report that.
|
|
119
|
+
doc.pop("deliverables", None)
|
|
120
|
+
for ctx in doc["contexts"]:
|
|
121
|
+
ctx.pop("deliverable", None)
|
|
122
|
+
doc["sources"]["schedule"]["align"]["clock"] = "file_mtime"
|
|
123
|
+
doc["id"] = "showcase-conference"
|
|
124
|
+
doc["name"] = "Example Conference 2026 - Live Video"
|
|
125
|
+
doc["vendors"] = VENDORS
|
|
126
|
+
(spec_dir / "spec.yaml").write_text(
|
|
127
|
+
yaml.safe_dump(doc, sort_keys=False, allow_unicode=True), encoding="utf-8"
|
|
128
|
+
)
|
|
129
|
+
(spec_dir / "agenda.csv").write_text(AGENDA, encoding="utf-8")
|
|
130
|
+
|
|
131
|
+
for rel, day, hhmm in FILES:
|
|
132
|
+
path = root / rel
|
|
133
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
size = 4096 if rel.endswith((".mov", ".mxf")) else 1024
|
|
135
|
+
path.write_bytes(b"\0" * size)
|
|
136
|
+
if hhmm:
|
|
137
|
+
h, m = (int(x) for x in hhmm.split(":"))
|
|
138
|
+
when = datetime(2026, 11, 9 + day, h, m, tzinfo=TZ).timestamp()
|
|
139
|
+
os.utime(path, (when, when))
|
|
140
|
+
return spec_dir / "spec.yaml"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def make(home: Path, *, fresh: bool = False) -> Path:
|
|
146
|
+
"""The sample drive in the app's folder, built once and kept.
|
|
147
|
+
|
|
148
|
+
`fresh` throws the old one away first, so somebody who renamed half of
|
|
149
|
+
it can start again. Only ever the folder this module made.
|
|
150
|
+
"""
|
|
151
|
+
target = Path(home) / "sample-drive"
|
|
152
|
+
if fresh and target.is_dir() and (target / ".shuttlecheck-sample").is_file():
|
|
153
|
+
shutil.rmtree(target)
|
|
154
|
+
if not (target / ".shuttlecheck-sample").is_file():
|
|
155
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
build(target)
|
|
157
|
+
(target / ".shuttlecheck-sample").write_text(
|
|
158
|
+
"A sample drive made by ShuttleCheck. Safe to delete.\n",
|
|
159
|
+
encoding="utf-8")
|
|
160
|
+
return target / "EXC26"
|