shuttlecheck 0.3.0__tar.gz → 0.4.0__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.3.0/shuttlecheck.egg-info → shuttlecheck-0.4.0}/PKG-INFO +19 -8
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/PYPI.md +16 -7
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/pyproject.toml +74 -67
- shuttlecheck-0.4.0/shuttlecheck/admission.py +292 -0
- shuttlecheck-0.4.0/shuttlecheck/ale.py +263 -0
- shuttlecheck-0.4.0/shuttlecheck/aliases.py +183 -0
- shuttlecheck-0.4.0/shuttlecheck/arrivals.py +659 -0
- shuttlecheck-0.4.0/shuttlecheck/attribution.py +172 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/auth.py +130 -10
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/checks.py +19 -5
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/cli.py +40 -3
- shuttlecheck-0.4.0/shuttlecheck/completeness.py +757 -0
- shuttlecheck-0.4.0/shuttlecheck/conventions.py +580 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/delivery.py +122 -14
- shuttlecheck-0.4.0/shuttlecheck/endpoints.py +481 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/infer.py +173 -15
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/jobs.py +326 -21
- shuttlecheck-0.4.0/shuttlecheck/journal.py +167 -0
- shuttlecheck-0.4.0/shuttlecheck/listener.py +317 -0
- shuttlecheck-0.4.0/shuttlecheck/marks.py +89 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/mhl.py +28 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/patterns.py +34 -0
- shuttlecheck-0.4.0/shuttlecheck/prefolder.py +163 -0
- shuttlecheck-0.4.0/shuttlecheck/probe.py +278 -0
- shuttlecheck-0.4.0/shuttlecheck/provenance.py +224 -0
- shuttlecheck-0.4.0/shuttlecheck/receive.py +1079 -0
- shuttlecheck-0.4.0/shuttlecheck/report.py +552 -0
- shuttlecheck-0.4.0/shuttlecheck/say.py +56 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/schema.json +34 -2
- shuttlecheck-0.4.0/shuttlecheck/seal.py +418 -0
- shuttlecheck-0.4.0/shuttlecheck/seal_api.py +201 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/sidecar.py +6 -4
- shuttlecheck-0.4.0/shuttlecheck/sizing.py +107 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/store.py +97 -12
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/support.py +2 -2
- shuttlecheck-0.4.0/shuttlecheck/templates.py +120 -0
- shuttlecheck-0.4.0/shuttlecheck/today.py +413 -0
- shuttlecheck-0.4.0/shuttlecheck/transport.py +540 -0
- shuttlecheck-0.4.0/shuttlecheck/ui.py +2366 -0
- shuttlecheck-0.4.0/shuttlecheck/ui_assets/app.css +596 -0
- shuttlecheck-0.4.0/shuttlecheck/ui_assets/app.html +50 -0
- shuttlecheck-0.4.0/shuttlecheck/ui_assets/app.js +2547 -0
- shuttlecheck-0.4.0/shuttlecheck/ui_assets/classic.css +3301 -0
- shuttlecheck-0.4.0/shuttlecheck/ui_assets/classic.html +115 -0
- shuttlecheck-0.4.0/shuttlecheck/ui_assets/classic.js +11165 -0
- shuttlecheck-0.4.0/shuttlecheck/ui_assets/fonts/display-LICENSE.txt +93 -0
- shuttlecheck-0.4.0/shuttlecheck/ui_assets/fonts/display.woff2 +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/validate.py +384 -20
- shuttlecheck-0.4.0/shuttlecheck/version.py +1 -0
- shuttlecheck-0.4.0/shuttlecheck/window.py +190 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0/shuttlecheck.egg-info}/PKG-INFO +19 -8
- shuttlecheck-0.4.0/shuttlecheck.egg-info/SOURCES.txt +150 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck.egg-info/requires.txt +3 -0
- shuttlecheck-0.4.0/tests/test_admission.py +279 -0
- shuttlecheck-0.4.0/tests/test_ale.py +164 -0
- shuttlecheck-0.4.0/tests/test_aliases.py +271 -0
- shuttlecheck-0.4.0/tests/test_allocate.py +401 -0
- shuttlecheck-0.4.0/tests/test_arrivals.py +705 -0
- shuttlecheck-0.4.0/tests/test_auth_packaged.py +64 -0
- shuttlecheck-0.4.0/tests/test_check_against.py +163 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_classify.py +58 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_cli.py +6 -2
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_completeness.py +197 -13
- shuttlecheck-0.4.0/tests/test_consoles.py +224 -0
- shuttlecheck-0.4.0/tests/test_conventions.py +254 -0
- shuttlecheck-0.4.0/tests/test_conventions_routes.py +183 -0
- shuttlecheck-0.4.0/tests/test_counting_files.py +176 -0
- shuttlecheck-0.4.0/tests/test_dailies.py +163 -0
- shuttlecheck-0.4.0/tests/test_dailiesdrop.py +379 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_delivery.py +151 -1
- shuttlecheck-0.4.0/tests/test_device_flow.py +213 -0
- shuttlecheck-0.4.0/tests/test_due.py +381 -0
- shuttlecheck-0.4.0/tests/test_endpoints.py +245 -0
- shuttlecheck-0.4.0/tests/test_enrolment.py +270 -0
- shuttlecheck-0.4.0/tests/test_fleet_units.py +206 -0
- shuttlecheck-0.4.0/tests/test_folders.py +164 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_groups.py +29 -2
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_infer_keys.py +7 -2
- shuttlecheck-0.4.0/tests/test_keepable.py +121 -0
- shuttlecheck-0.4.0/tests/test_lamps.py +380 -0
- shuttlecheck-0.4.0/tests/test_listener.py +233 -0
- shuttlecheck-0.4.0/tests/test_marks_api.py +76 -0
- shuttlecheck-0.4.0/tests/test_message_style.py +118 -0
- shuttlecheck-0.4.0/tests/test_meters_measure.py +138 -0
- shuttlecheck-0.4.0/tests/test_monitor.py +343 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_no_convention.py +9 -3
- shuttlecheck-0.4.0/tests/test_one_file_is_not_a_pattern.py +174 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_originals.py +1 -1
- shuttlecheck-0.4.0/tests/test_pointed_at_the_job.py +140 -0
- shuttlecheck-0.4.0/tests/test_prefolder.py +91 -0
- shuttlecheck-0.4.0/tests/test_problems.py +164 -0
- shuttlecheck-0.4.0/tests/test_provenance.py +214 -0
- shuttlecheck-0.4.0/tests/test_rack.py +2218 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_receive.py +54 -12
- shuttlecheck-0.4.0/tests/test_report.py +75 -0
- shuttlecheck-0.4.0/tests/test_reveal.py +155 -0
- shuttlecheck-0.4.0/tests/test_rollcall_schedule.py +336 -0
- shuttlecheck-0.4.0/tests/test_signal_path.py +653 -0
- shuttlecheck-0.4.0/tests/test_sizing.py +206 -0
- shuttlecheck-0.4.0/tests/test_sizing_history.py +175 -0
- shuttlecheck-0.4.0/tests/test_source_attach.py +216 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_store.py +40 -2
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_support.py +9 -2
- shuttlecheck-0.4.0/tests/test_sweeping.py +189 -0
- shuttlecheck-0.4.0/tests/test_templates.py +120 -0
- shuttlecheck-0.4.0/tests/test_the_04_window.py +172 -0
- shuttlecheck-0.4.0/tests/test_the_analyser.py +426 -0
- shuttlecheck-0.4.0/tests/test_the_pipeline.py +2006 -0
- shuttlecheck-0.4.0/tests/test_today.py +670 -0
- shuttlecheck-0.4.0/tests/test_today_screen.py +210 -0
- shuttlecheck-0.4.0/tests/test_trademark.py +212 -0
- shuttlecheck-0.4.0/tests/test_transport.py +394 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_ui_routes.py +71 -2
- shuttlecheck-0.4.0/tests/test_unread_sources.py +143 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_validate.py +13 -3
- shuttlecheck-0.4.0/tests/test_walk.py +313 -0
- shuttlecheck-0.4.0/tests/test_web_seal.py +241 -0
- shuttlecheck-0.4.0/tests/test_what_the_far_end_reads.py +95 -0
- shuttlecheck-0.4.0/tests/test_where_it_belongs.py +396 -0
- shuttlecheck-0.4.0/tests/test_window.py +293 -0
- shuttlecheck-0.3.0/shuttlecheck/completeness.py +0 -227
- shuttlecheck-0.3.0/shuttlecheck/receive.py +0 -517
- shuttlecheck-0.3.0/shuttlecheck/report.py +0 -218
- shuttlecheck-0.3.0/shuttlecheck/ui.py +0 -868
- shuttlecheck-0.3.0/shuttlecheck/ui_assets/app.css +0 -525
- shuttlecheck-0.3.0/shuttlecheck/ui_assets/app.html +0 -26
- shuttlecheck-0.3.0/shuttlecheck/ui_assets/app.js +0 -2361
- shuttlecheck-0.3.0/shuttlecheck/version.py +0 -1
- shuttlecheck-0.3.0/shuttlecheck.egg-info/SOURCES.txt +0 -70
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/LICENSE +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/README.md +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/setup.cfg +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/__init__.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/extensions.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/originals.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/schedule.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/spec.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/telemetry-v1.schema.json +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/telemetry.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/ui_assets/fonts/inter-LICENSE.txt +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/ui_assets/fonts/inter.woff2 +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/ui_assets/fonts/mono-LICENSE.txt +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/ui_assets/fonts/mono.woff2 +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/updates.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck/walkthrough.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck.egg-info/dependency_links.txt +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck.egg-info/entry_points.txt +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/shuttlecheck.egg-info/top_level.txt +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_auth.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_fonts.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_infer.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_jobs.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_mhl.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_min_version.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_path_length.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_pick.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_rename_token.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_telemetry.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_ui.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_updates.py +0 -0
- {shuttlecheck-0.3.0 → shuttlecheck-0.4.0}/tests/test_verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shuttlecheck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -30,9 +30,11 @@ Requires-Dist: xxhash>=3.0
|
|
|
30
30
|
Requires-Dist: tzdata>=2024.1; sys_platform == "win32"
|
|
31
31
|
Provides-Extra: dev
|
|
32
32
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
33
|
+
Provides-Extra: app
|
|
34
|
+
Requires-Dist: pywebview>=5.0; extra == "app"
|
|
33
35
|
Dynamic: license-file
|
|
34
36
|
|
|
35
|
-
# ShuttleCheck
|
|
37
|
+
# ShuttleCheck™
|
|
36
38
|
|
|
37
39
|
**Check media filenames, folder structure and deliverables against a written
|
|
38
40
|
convention.** Nothing in this package renames, moves or deletes anything.
|
|
@@ -130,13 +132,18 @@ No spec to be told about, no account, no connection.
|
|
|
130
132
|
|
|
131
133
|
## What this package will not do
|
|
132
134
|
|
|
133
|
-
It will not rename, move or delete anything. That boundary is the
|
|
134
|
-
the command a stranger will run on a live job, because running
|
|
135
|
-
them anything.
|
|
135
|
+
It will not rename, move or delete anything of yours. That boundary is the
|
|
136
|
+
point: it is the command a stranger will run on a live job, because running
|
|
137
|
+
it cannot cost them anything.
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
**Sealing is the one exception, and it is free.** `seal` writes an ASC MHL
|
|
140
|
+
hash list and the convention alongside your media, touching none of it. A
|
|
141
|
+
manifest is only worth what the far end can read for nothing, so the half
|
|
142
|
+
that writes one cannot be the paid half.
|
|
143
|
+
|
|
144
|
+
Conforming files to a convention, and undoing a conform run, are licensed
|
|
145
|
+
separately as ShuttleCheck Pro. Asking for one of those commands here prints
|
|
146
|
+
an explanation rather than an error.
|
|
140
147
|
|
|
141
148
|
## Licence
|
|
142
149
|
|
|
@@ -144,3 +151,7 @@ Free to install and use. Not open source: no right to redistribute or modify.
|
|
|
144
151
|
See <https://mvrk.systems/shuttlecheck>.
|
|
145
152
|
|
|
146
153
|
Made by [MVRK Systems](https://mvrk.systems).
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
ShuttleCheck™ and DailiesDrop™ are trademarks of MVRK Systems.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ShuttleCheck
|
|
1
|
+
# ShuttleCheck™
|
|
2
2
|
|
|
3
3
|
**Check media filenames, folder structure and deliverables against a written
|
|
4
4
|
convention.** Nothing in this package renames, moves or deletes anything.
|
|
@@ -96,13 +96,18 @@ No spec to be told about, no account, no connection.
|
|
|
96
96
|
|
|
97
97
|
## What this package will not do
|
|
98
98
|
|
|
99
|
-
It will not rename, move or delete anything. That boundary is the
|
|
100
|
-
the command a stranger will run on a live job, because running
|
|
101
|
-
them anything.
|
|
99
|
+
It will not rename, move or delete anything of yours. That boundary is the
|
|
100
|
+
point: it is the command a stranger will run on a live job, because running
|
|
101
|
+
it cannot cost them anything.
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
**Sealing is the one exception, and it is free.** `seal` writes an ASC MHL
|
|
104
|
+
hash list and the convention alongside your media, touching none of it. A
|
|
105
|
+
manifest is only worth what the far end can read for nothing, so the half
|
|
106
|
+
that writes one cannot be the paid half.
|
|
107
|
+
|
|
108
|
+
Conforming files to a convention, and undoing a conform run, are licensed
|
|
109
|
+
separately as ShuttleCheck Pro. Asking for one of those commands here prints
|
|
110
|
+
an explanation rather than an error.
|
|
106
111
|
|
|
107
112
|
## Licence
|
|
108
113
|
|
|
@@ -110,3 +115,7 @@ Free to install and use. Not open source: no right to redistribute or modify.
|
|
|
110
115
|
See <https://mvrk.systems/shuttlecheck>.
|
|
111
116
|
|
|
112
117
|
Made by [MVRK Systems](https://mvrk.systems).
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
ShuttleCheck™ and DailiesDrop™ are trademarks of MVRK Systems.
|
|
@@ -1,67 +1,74 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "shuttlecheck"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Check media filenames, folder structure and deliverables against a written convention. Read-only."
|
|
5
|
-
readme = { file = "PYPI.md", content-type = "text/markdown" }
|
|
6
|
-
requires-python = ">=3.11"
|
|
7
|
-
license = { text = "Proprietary — free to use, no redistribution" }
|
|
8
|
-
authors = [{ name = "MVRK Systems", email = "hello@mvrk.systems" }]
|
|
9
|
-
keywords = [
|
|
10
|
-
"broadcast", "post-production", "dailies", "media", "naming-convention",
|
|
11
|
-
"file-naming", "delivery", "conformance", "validation", "vfx", "video",
|
|
12
|
-
]
|
|
13
|
-
classifiers = [
|
|
14
|
-
"Development Status :: 4 - Beta",
|
|
15
|
-
"Environment :: Console",
|
|
16
|
-
"Environment :: Web Environment",
|
|
17
|
-
"Intended Audience :: End Users/Desktop",
|
|
18
|
-
"License :: Other/Proprietary License",
|
|
19
|
-
"Natural Language :: English",
|
|
20
|
-
"Operating System :: OS Independent",
|
|
21
|
-
"Programming Language :: Python :: 3",
|
|
22
|
-
"Programming Language :: Python :: 3.11",
|
|
23
|
-
"Programming Language :: Python :: 3.12",
|
|
24
|
-
"Programming Language :: Python :: 3.13",
|
|
25
|
-
"Topic :: Multimedia :: Video",
|
|
26
|
-
"Topic :: System :: Archiving",
|
|
27
|
-
"Topic :: Utilities",
|
|
28
|
-
]
|
|
29
|
-
dependencies = [
|
|
30
|
-
"PyYAML>=6.0",
|
|
31
|
-
"jsonschema>=4.20",
|
|
32
|
-
# xxh64 is the checksum the ASC MHL specification is usually written with
|
|
33
|
-
# and the one DailiesDrop records, and it is not in the standard library.
|
|
34
|
-
# A dependency in the read-only half looks like the wrong place for it
|
|
35
|
-
# until you notice that verifying a drive the appliance sealed is exactly
|
|
36
|
-
# what this half is for: without it, the common case degrades to "this
|
|
37
|
-
# installation cannot check those checksums".
|
|
38
|
-
"xxhash>=3.0",
|
|
39
|
-
# Windows ships no system tz database, so zoneinfo cannot resolve the
|
|
40
|
-
# agenda timezone without this. Required, not optional, on a target platform.
|
|
41
|
-
"tzdata>=2024.1; sys_platform == 'win32'",
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
[project.optional-dependencies]
|
|
45
|
-
dev = ["pytest>=8.0"]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
[
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
[
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1
|
+
[project]
|
|
2
|
+
name = "shuttlecheck"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "Check media filenames, folder structure and deliverables against a written convention. Read-only."
|
|
5
|
+
readme = { file = "PYPI.md", content-type = "text/markdown" }
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "Proprietary — free to use, no redistribution" }
|
|
8
|
+
authors = [{ name = "MVRK Systems", email = "hello@mvrk.systems" }]
|
|
9
|
+
keywords = [
|
|
10
|
+
"broadcast", "post-production", "dailies", "media", "naming-convention",
|
|
11
|
+
"file-naming", "delivery", "conformance", "validation", "vfx", "video",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Environment :: Web Environment",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"License :: Other/Proprietary License",
|
|
19
|
+
"Natural Language :: English",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Multimedia :: Video",
|
|
26
|
+
"Topic :: System :: Archiving",
|
|
27
|
+
"Topic :: Utilities",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"PyYAML>=6.0",
|
|
31
|
+
"jsonschema>=4.20",
|
|
32
|
+
# xxh64 is the checksum the ASC MHL specification is usually written with
|
|
33
|
+
# and the one DailiesDrop records, and it is not in the standard library.
|
|
34
|
+
# A dependency in the read-only half looks like the wrong place for it
|
|
35
|
+
# until you notice that verifying a drive the appliance sealed is exactly
|
|
36
|
+
# what this half is for: without it, the common case degrades to "this
|
|
37
|
+
# installation cannot check those checksums".
|
|
38
|
+
"xxhash>=3.0",
|
|
39
|
+
# Windows ships no system tz database, so zoneinfo cannot resolve the
|
|
40
|
+
# agenda timezone without this. Required, not optional, on a target platform.
|
|
41
|
+
"tzdata>=2024.1; sys_platform == 'win32'",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
dev = ["pytest>=8.0"]
|
|
46
|
+
# The downloadable tool's own window, drawn by whatever webview the operating
|
|
47
|
+
# system already has: WebView2 on Windows, WKWebView on macOS, WebKitGTK on
|
|
48
|
+
# Linux. An extra rather than a dependency, because `pip install shuttlecheck`
|
|
49
|
+
# is a library and a CLI and should not pull in a GUI stack, and because the
|
|
50
|
+
# app falls back to a browser wherever this is missing. See
|
|
51
|
+
# shuttlecheck/window.py.
|
|
52
|
+
app = ["pywebview>=5.0"]
|
|
53
|
+
|
|
54
|
+
[project.urls]
|
|
55
|
+
Homepage = "https://mvrk.systems/shuttlecheck"
|
|
56
|
+
Documentation = "https://mvrk.systems/shuttlecheck#checks"
|
|
57
|
+
|
|
58
|
+
[project.scripts]
|
|
59
|
+
shuttlecheck = "shuttlecheck.cli:main"
|
|
60
|
+
|
|
61
|
+
[build-system]
|
|
62
|
+
requires = ["setuptools>=68"]
|
|
63
|
+
build-backend = "setuptools.build_meta"
|
|
64
|
+
|
|
65
|
+
[tool.setuptools]
|
|
66
|
+
packages = ["shuttlecheck"]
|
|
67
|
+
|
|
68
|
+
[tool.setuptools.package-data]
|
|
69
|
+
# Neither schema is documentation. The validator refuses to run without the
|
|
70
|
+
# spec schema, and the telemetry contract is checked against every batch before
|
|
71
|
+
# it is sent, so a build that cannot read it sends nothing at all. Both travel
|
|
72
|
+
# inside the wheel.
|
|
73
|
+
shuttlecheck = ["schema.json", "telemetry-v1.schema.json", "ui_assets/*",
|
|
74
|
+
"ui_assets/fonts/*"]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Letting a unit in, on the local network, with no service in the middle.
|
|
2
|
+
|
|
3
|
+
`docs/TRANSPORT.md` section 3a.
|
|
4
|
+
|
|
5
|
+
**Why this exists beside enrolment.** The fleet service is how a unit finds a
|
|
6
|
+
destination it has never met, across networks. It is not how a unit and a
|
|
7
|
+
destination on the same switch meet, and requiring it there would mean a
|
|
8
|
+
machine in a basement with no route out cannot take delivery from a unit two
|
|
9
|
+
metres away. It can.
|
|
10
|
+
|
|
11
|
+
**Why this is not `endpoints.pair`.** Pairing names a unit: a serial and a
|
|
12
|
+
label off the front of the box, a statement about what this destination
|
|
13
|
+
expects. Its own docstring says it is deliberately not a connection. Admitting
|
|
14
|
+
is the separate step that gives a named unit something it can present, and
|
|
15
|
+
keeping the two apart is the difference between "we expect this serial" and
|
|
16
|
+
"this caller has proved it is that serial". The screens say it that way round
|
|
17
|
+
too.
|
|
18
|
+
|
|
19
|
+
**What a token is worth.** It authenticates a unit to this destination and
|
|
20
|
+
nothing else. It is not an account, it does not reach the fleet, it cannot
|
|
21
|
+
mint anything, and it is useless on any other machine. It never reaches a
|
|
22
|
+
screen; the admission code does, because a code is made to be read aloud.
|
|
23
|
+
|
|
24
|
+
**What a code is worth.** Seven characters, one use, ten minutes, five wrong
|
|
25
|
+
answers and the window shuts. That is deliberately not a password: it is the
|
|
26
|
+
few seconds in which somebody standing at the destination says a unit may come
|
|
27
|
+
in. Nothing is reachable through it once it closes.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import hmac
|
|
33
|
+
import secrets
|
|
34
|
+
import time
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
|
|
37
|
+
from . import endpoints, store
|
|
38
|
+
|
|
39
|
+
# No O, 0, I or 1. The code is read out across a machine room, and the two
|
|
40
|
+
# pairs people mishear are the two pairs that are not in here.
|
|
41
|
+
ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
|
|
42
|
+
CODE_LENGTH = 7
|
|
43
|
+
|
|
44
|
+
# Long enough to walk to the unit and type it, short enough that a window
|
|
45
|
+
# somebody forgot about is not an open door by the afternoon.
|
|
46
|
+
WINDOW_SECONDS = 600
|
|
47
|
+
|
|
48
|
+
# A seven character code out of this alphabet is one in thirty four billion.
|
|
49
|
+
# The cap is not arithmetic, it is so that a window being hammered closes
|
|
50
|
+
# rather than quietly standing there being hammered.
|
|
51
|
+
MAX_ATTEMPTS = 5
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _path():
|
|
55
|
+
return store.home() / "admissions.json"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read() -> dict:
|
|
59
|
+
row = store._read_json(_path(), {})
|
|
60
|
+
if not isinstance(row, dict):
|
|
61
|
+
return {}
|
|
62
|
+
if not isinstance(row.get("units"), dict):
|
|
63
|
+
row["units"] = {}
|
|
64
|
+
return row
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _write(row: dict) -> None:
|
|
68
|
+
store._write_json(_path(), row)
|
|
69
|
+
store._restrict(_path())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _now() -> str:
|
|
73
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------------------------
|
|
77
|
+
# The window
|
|
78
|
+
# --------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def _live(row: dict) -> dict:
|
|
81
|
+
"""The window, if there is one and it has not run out."""
|
|
82
|
+
win = row.get("window")
|
|
83
|
+
if not isinstance(win, dict):
|
|
84
|
+
return {}
|
|
85
|
+
if float(win.get("expires") or 0) <= time.time():
|
|
86
|
+
return {}
|
|
87
|
+
if int(win.get("attempts") or 0) >= MAX_ATTEMPTS:
|
|
88
|
+
return {}
|
|
89
|
+
return win
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def open_window(serial: str = "") -> dict:
|
|
93
|
+
"""Start the few minutes in which a unit may be admitted.
|
|
94
|
+
|
|
95
|
+
A window may name a serial or not. Named, only that unit may use it, which
|
|
96
|
+
is what you want when four units are in the same rack. Unnamed, the first
|
|
97
|
+
unit to answer is admitted and the serial it declares is recorded.
|
|
98
|
+
|
|
99
|
+
Opening a second window replaces the first. Two codes live at once would
|
|
100
|
+
mean somebody reading out the one that has already been superseded.
|
|
101
|
+
"""
|
|
102
|
+
serial = endpoints.normalise_serial(serial)
|
|
103
|
+
if serial and not endpoints.SERIAL.match(serial):
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"a unit serial looks like {endpoints.SERIAL_SHAPE} and is "
|
|
106
|
+
f"printed on the front of the appliance")
|
|
107
|
+
|
|
108
|
+
code = "".join(secrets.choice(ALPHABET) for _ in range(CODE_LENGTH))
|
|
109
|
+
row = _read()
|
|
110
|
+
row["window"] = {
|
|
111
|
+
"code": code,
|
|
112
|
+
"serial": serial,
|
|
113
|
+
"opened": _now(),
|
|
114
|
+
"expires": time.time() + WINDOW_SECONDS,
|
|
115
|
+
"attempts": 0,
|
|
116
|
+
}
|
|
117
|
+
_write(row)
|
|
118
|
+
|
|
119
|
+
# The port has to be answering for a code to be worth anything. This is
|
|
120
|
+
# one of the three states that mean this machine will answer a unit, so
|
|
121
|
+
# the listener follows it the way it follows the other two.
|
|
122
|
+
from . import listener
|
|
123
|
+
listener.start()
|
|
124
|
+
return window()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def window() -> dict:
|
|
128
|
+
"""What the screen shows. The code is in here on purpose.
|
|
129
|
+
|
|
130
|
+
A token never reaches a screen. A code is made to be read aloud, lasts ten
|
|
131
|
+
minutes and opens nothing once it is used, so showing it is the whole of
|
|
132
|
+
what it is for.
|
|
133
|
+
"""
|
|
134
|
+
win = _live(_read())
|
|
135
|
+
if not win:
|
|
136
|
+
return {"open": False}
|
|
137
|
+
return {
|
|
138
|
+
"open": True,
|
|
139
|
+
"code": win["code"],
|
|
140
|
+
"serial": win.get("serial", ""),
|
|
141
|
+
"secondsLeft": max(0, int(float(win["expires"]) - time.time())),
|
|
142
|
+
"attemptsLeft": MAX_ATTEMPTS - int(win.get("attempts") or 0),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def close_window() -> dict:
|
|
147
|
+
row = _read()
|
|
148
|
+
row.pop("window", None)
|
|
149
|
+
_write(row)
|
|
150
|
+
_follow()
|
|
151
|
+
return {"open": False}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _bump() -> None:
|
|
155
|
+
"""Count a failed attempt against the window, if one is open."""
|
|
156
|
+
row = _read()
|
|
157
|
+
win = row.get("window")
|
|
158
|
+
if isinstance(win, dict):
|
|
159
|
+
win["attempts"] = int(win.get("attempts") or 0) + 1
|
|
160
|
+
row["window"] = win
|
|
161
|
+
_write(row)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------------------
|
|
165
|
+
# Admitting
|
|
166
|
+
# --------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def admit(code: str, serial: str, label: str = "") -> dict:
|
|
169
|
+
"""Spend the window and issue this unit a token.
|
|
170
|
+
|
|
171
|
+
Returns the unit's half of the answer, or `{}` for every kind of no. The
|
|
172
|
+
caller is a stranger on the network and the four ways to fail here, a
|
|
173
|
+
wrong code, a closed window, the wrong serial for a named window, and a
|
|
174
|
+
serial that is not shaped like one, are all the same answer to it. A
|
|
175
|
+
caller that could tell them apart could map the difference between a
|
|
176
|
+
window that is open and one that is not.
|
|
177
|
+
"""
|
|
178
|
+
row = _read()
|
|
179
|
+
win = _live(row)
|
|
180
|
+
if not win:
|
|
181
|
+
return {}
|
|
182
|
+
|
|
183
|
+
serial = endpoints.normalise_serial(serial)
|
|
184
|
+
presented = str(code or "").strip().upper()
|
|
185
|
+
|
|
186
|
+
# Constant time on the code, and the serial checks run whatever the code
|
|
187
|
+
# did, so no branch here times differently from another.
|
|
188
|
+
code_ok = hmac.compare_digest(presented, str(win.get("code") or ""))
|
|
189
|
+
shape_ok = bool(endpoints.SERIAL.match(serial))
|
|
190
|
+
named = str(win.get("serial") or "")
|
|
191
|
+
serial_ok = (not named) or hmac.compare_digest(serial, named)
|
|
192
|
+
|
|
193
|
+
if not (code_ok and shape_ok and serial_ok):
|
|
194
|
+
_bump()
|
|
195
|
+
return {}
|
|
196
|
+
|
|
197
|
+
token = secrets.token_urlsafe(32)
|
|
198
|
+
units = row.get("units") or {}
|
|
199
|
+
units[serial] = {
|
|
200
|
+
"token": token,
|
|
201
|
+
"at": _now(),
|
|
202
|
+
"label": endpoints._screen_safe(label) or "",
|
|
203
|
+
}
|
|
204
|
+
row["units"] = units
|
|
205
|
+
row.pop("window", None)
|
|
206
|
+
_write(row)
|
|
207
|
+
|
|
208
|
+
# An admitted unit is a unit this destination expects. Two lists that can
|
|
209
|
+
# disagree about that would be one list too many.
|
|
210
|
+
if not any(u.serial == serial for u in endpoints.units()):
|
|
211
|
+
try:
|
|
212
|
+
endpoints.pair(serial, endpoints._screen_safe(label))
|
|
213
|
+
except ValueError:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"token": token,
|
|
218
|
+
"serial": serial,
|
|
219
|
+
"destination": endpoints.destination_id(),
|
|
220
|
+
"machine": _machine(),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _machine() -> str:
|
|
225
|
+
from . import receive
|
|
226
|
+
try:
|
|
227
|
+
return receive.machine_name()
|
|
228
|
+
except Exception: # noqa: BLE001
|
|
229
|
+
return ""
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# --------------------------------------------------------------------------
|
|
233
|
+
# What the listener asks
|
|
234
|
+
# --------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def serial_for_token(token: str) -> str:
|
|
237
|
+
"""Which admitted unit presented this, or empty.
|
|
238
|
+
|
|
239
|
+
Every held token is compared, and the comparison does not stop at the
|
|
240
|
+
first match, so the time this takes says nothing about which unit it was
|
|
241
|
+
or whether it matched at all.
|
|
242
|
+
"""
|
|
243
|
+
presented = str(token or "")
|
|
244
|
+
if not presented:
|
|
245
|
+
return ""
|
|
246
|
+
found = ""
|
|
247
|
+
for serial, row in (_read().get("units") or {}).items():
|
|
248
|
+
if not isinstance(row, dict):
|
|
249
|
+
continue
|
|
250
|
+
if hmac.compare_digest(presented, str(row.get("token") or "")):
|
|
251
|
+
found = serial
|
|
252
|
+
return found
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def admitted() -> list:
|
|
256
|
+
"""The serials that hold a token, oldest first."""
|
|
257
|
+
rows = (_read().get("units") or {}).items()
|
|
258
|
+
return [s for s, _ in sorted(rows, key=lambda kv: str(
|
|
259
|
+
kv[1].get("at", "") if isinstance(kv[1], dict) else ""))]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def any_admitted() -> bool:
|
|
263
|
+
return bool(_read().get("units"))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def is_admitted(serial: str) -> bool:
|
|
267
|
+
return endpoints.normalise_serial(serial) in (_read().get("units") or {})
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def revoke(serial: str) -> bool:
|
|
271
|
+
"""Discard a unit's token. The unit is not told.
|
|
272
|
+
|
|
273
|
+
It finds out when its next request is refused, which is the only way a
|
|
274
|
+
revocation can work on a link that may be down.
|
|
275
|
+
"""
|
|
276
|
+
serial = endpoints.normalise_serial(serial)
|
|
277
|
+
row = _read()
|
|
278
|
+
units = row.get("units") or {}
|
|
279
|
+
if serial not in units:
|
|
280
|
+
return False
|
|
281
|
+
units.pop(serial)
|
|
282
|
+
row["units"] = units
|
|
283
|
+
_write(row)
|
|
284
|
+
_follow()
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _follow() -> None:
|
|
289
|
+
"""Close the port if nothing holds it open any more."""
|
|
290
|
+
from . import listener, receive
|
|
291
|
+
if not receive.receiving():
|
|
292
|
+
listener.stop()
|