c4py 1.0.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.
c4py/__init__.py ADDED
@@ -0,0 +1,95 @@
1
+ """c4py — Pure Python C4 universal content identification (SMPTE ST 2114:2017).
2
+
3
+ import c4py
4
+
5
+ # Identify content
6
+ c4id = c4py.identify(open("file.mov", "rb"))
7
+ c4id = c4py.identify_bytes(b"hello")
8
+ c4id = c4py.parse("c45xZeXwMSpq...")
9
+
10
+ # C4M manifests
11
+ manifest = c4py.scan("/projects/HERO")
12
+ manifest = c4py.load("project.c4m")
13
+ diff = c4py.diff(old, new)
14
+
15
+ # Content store (shared with c4 CLI and c4sh)
16
+ store = c4py.open_store("~/.c4/store")
17
+ c4id = store.put(open("render.exr", "rb"))
18
+ content = store.get(c4id)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from .id import C4ID, identify, identify_bytes, identify_file, identify_files, verify, parse, tree_id
24
+ from .manifest import Manifest
25
+ from .entry import Entry
26
+ from .diff import (
27
+ Conflict, DiffResult, PatchInfo,
28
+ diff, patch_diff, apply_patch, resolve_chain, log_chain, merge,
29
+ )
30
+ from .decoder import load, loads
31
+ from .encoder import dump, dumps
32
+ from .scanner import scan
33
+ from .store import ContentNotFound, FSStore, Store, open_store
34
+ from .reconcile import ReconcileOp, ReconcilePlan, ReconcileResult, reconcile
35
+ from .pool import IngestResult, PoolResult, ingest, pool
36
+ from .validator import ValidationResult, validate
37
+ from .verify import CorruptEntry, VerifyReport, verify_tree
38
+ from .workspace import Workspace
39
+
40
+ __version__ = "1.0.0"
41
+
42
+ __all__ = [
43
+ # Identification
44
+ "C4ID",
45
+ "identify",
46
+ "identify_bytes",
47
+ "identify_file",
48
+ "identify_files",
49
+ "verify",
50
+ "parse",
51
+ "tree_id",
52
+ # Manifest
53
+ "Manifest",
54
+ "Entry",
55
+ # I/O
56
+ "load",
57
+ "loads",
58
+ "dump",
59
+ "dumps",
60
+ "scan",
61
+ # Content Store
62
+ "Store",
63
+ "FSStore",
64
+ "ContentNotFound",
65
+ "open_store",
66
+ # Operations
67
+ "Conflict",
68
+ "DiffResult",
69
+ "PatchInfo",
70
+ "diff",
71
+ "merge",
72
+ "patch_diff",
73
+ "apply_patch",
74
+ "resolve_chain",
75
+ "log_chain",
76
+ # Reconcile
77
+ "ReconcileOp",
78
+ "ReconcilePlan",
79
+ "ReconcileResult",
80
+ "reconcile",
81
+ # Pool / Ingest
82
+ "PoolResult",
83
+ "IngestResult",
84
+ "pool",
85
+ "ingest",
86
+ # Validation
87
+ "ValidationResult",
88
+ "validate",
89
+ # Verification
90
+ "CorruptEntry",
91
+ "VerifyReport",
92
+ "verify_tree",
93
+ # Workspace
94
+ "Workspace",
95
+ ]
c4py/__main__.py ADDED
@@ -0,0 +1,228 @@
1
+ """CLI entry point for c4py — invoked via `python -m c4py`.
2
+
3
+ Commands:
4
+ id <path> Identify file/directory, output c4m
5
+ id -i <path> Bare C4 ID only (no c4m format)
6
+ diff <old> <new> Compare two c4m manifests
7
+ verify <c4m> <dir> Verify directory matches manifest
8
+ cat <c4id> Retrieve content from store
9
+ version Print version
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def _cmd_id(args: argparse.Namespace) -> int:
20
+ """Identify a file or directory."""
21
+ from . import __version__ # noqa: F401
22
+ from .id import identify_file
23
+ from .scanner import scan
24
+ from .encoder import dumps
25
+
26
+ target = Path(args.path)
27
+
28
+ if not target.exists():
29
+ print(f"error: {args.path}: no such file or directory", file=sys.stderr)
30
+ return 1
31
+
32
+ if args.id_only:
33
+ # Bare C4 ID mode
34
+ if target.is_file():
35
+ c4id = identify_file(target)
36
+ print(str(c4id))
37
+ elif target.is_dir():
38
+ manifest = scan(target)
39
+ c4id = manifest.compute_c4id()
40
+ print(str(c4id))
41
+ else:
42
+ print(f"error: {args.path}: unsupported file type", file=sys.stderr)
43
+ return 1
44
+ else:
45
+ # Full c4m output
46
+ if target.is_file():
47
+ c4id = identify_file(target)
48
+ print(str(c4id))
49
+ elif target.is_dir():
50
+ manifest = scan(target)
51
+ text = dumps(manifest)
52
+ sys.stdout.write(text)
53
+ else:
54
+ print(f"error: {args.path}: unsupported file type", file=sys.stderr)
55
+ return 1
56
+
57
+ return 0
58
+
59
+
60
+ def _cmd_diff(args: argparse.Namespace) -> int:
61
+ """Compare two c4m manifests."""
62
+ from .decoder import load
63
+ from .diff import diff
64
+
65
+ old_path = args.old
66
+ new_path = args.new
67
+
68
+ for p in (old_path, new_path):
69
+ if not Path(p).exists():
70
+ print(f"error: {p}: no such file", file=sys.stderr)
71
+ return 1
72
+
73
+ old_manifest = load(old_path)
74
+ new_manifest = load(new_path)
75
+ result = diff(old_manifest, new_manifest)
76
+
77
+ if result.is_empty:
78
+ print("no differences")
79
+ return 0
80
+
81
+ if result.added:
82
+ for entry in result.added:
83
+ print(f"+ {entry.name}")
84
+ if result.removed:
85
+ for entry in result.removed:
86
+ print(f"- {entry.name}")
87
+ if result.modified:
88
+ for entry in result.modified:
89
+ print(f"~ {entry.name}")
90
+
91
+ return 0
92
+
93
+
94
+ def _cmd_verify(args: argparse.Namespace) -> int:
95
+ """Verify a directory matches a manifest."""
96
+ from .verify import verify_tree
97
+
98
+ manifest_path = args.manifest
99
+ directory = args.directory
100
+
101
+ if not Path(manifest_path).exists():
102
+ print(f"error: {manifest_path}: no such file", file=sys.stderr)
103
+ return 1
104
+ if not Path(directory).is_dir():
105
+ print(f"error: {directory}: not a directory", file=sys.stderr)
106
+ return 1
107
+
108
+ report = verify_tree(manifest_path, directory)
109
+
110
+ if report.is_ok:
111
+ print(f"OK: {len(report.ok)} files verified")
112
+ return 0
113
+
114
+ if report.ok:
115
+ print(f"ok: {len(report.ok)} files match")
116
+ if report.missing:
117
+ print(f"missing: {len(report.missing)} files")
118
+ for p in report.missing:
119
+ print(f" - {p}")
120
+ if report.corrupt:
121
+ print(f"corrupt: {len(report.corrupt)} files")
122
+ for c in report.corrupt:
123
+ print(f" ! {c.path}")
124
+ print(f" expected: {c.expected}")
125
+ print(f" actual: {c.actual}")
126
+ if report.extra:
127
+ print(f"extra: {len(report.extra)} files")
128
+ for p in report.extra:
129
+ print(f" + {p}")
130
+
131
+ return 1
132
+
133
+
134
+ def _cmd_cat(args: argparse.Namespace) -> int:
135
+ """Retrieve content from store by C4 ID."""
136
+ from .id import parse
137
+ from .store import ContentNotFound, open_store
138
+
139
+ try:
140
+ c4id = parse(args.c4id)
141
+ except ValueError as e:
142
+ print(f"error: invalid C4 ID: {e}", file=sys.stderr)
143
+ return 1
144
+
145
+ try:
146
+ store = open_store()
147
+ except ValueError as e:
148
+ print(f"error: {e}", file=sys.stderr)
149
+ return 1
150
+
151
+ try:
152
+ stream = store.get(c4id)
153
+ except ContentNotFound:
154
+ print(f"error: content not found: {args.c4id}", file=sys.stderr)
155
+ return 1
156
+
157
+ try:
158
+ stdout_bin = sys.stdout.buffer
159
+ while True:
160
+ chunk = stream.read(65536)
161
+ if not chunk:
162
+ break
163
+ stdout_bin.write(chunk)
164
+ finally:
165
+ stream.close()
166
+
167
+ return 0
168
+
169
+
170
+ def _cmd_version(_args: argparse.Namespace) -> int:
171
+ """Print version."""
172
+ from . import __version__
173
+ print(f"c4py {__version__}")
174
+ return 0
175
+
176
+
177
+ def main(argv: list[str] | None = None) -> int:
178
+ """Parse arguments and dispatch to the appropriate subcommand."""
179
+ parser = argparse.ArgumentParser(
180
+ prog="c4py",
181
+ description="C4 universal content identification — SMPTE ST 2114:2017",
182
+ )
183
+ subparsers = parser.add_subparsers(dest="command")
184
+
185
+ # id
186
+ id_parser = subparsers.add_parser("id", help="identify file or directory")
187
+ id_parser.add_argument("path", help="file or directory to identify")
188
+ id_parser.add_argument(
189
+ "-i", "--id-only", action="store_true",
190
+ help="output bare C4 ID only (no c4m format)",
191
+ )
192
+
193
+ # diff
194
+ diff_parser = subparsers.add_parser("diff", help="compare two c4m manifests")
195
+ diff_parser.add_argument("old", help="old manifest path")
196
+ diff_parser.add_argument("new", help="new manifest path")
197
+
198
+ # verify
199
+ verify_parser = subparsers.add_parser("verify", help="verify directory matches manifest")
200
+ verify_parser.add_argument("manifest", help="c4m manifest path")
201
+ verify_parser.add_argument("directory", help="directory to verify")
202
+
203
+ # cat
204
+ cat_parser = subparsers.add_parser("cat", help="retrieve content from store")
205
+ cat_parser.add_argument("c4id", help="C4 ID to retrieve")
206
+
207
+ # version
208
+ subparsers.add_parser("version", help="print version")
209
+
210
+ args = parser.parse_args(argv)
211
+
212
+ if args.command is None:
213
+ parser.print_help()
214
+ return 1
215
+
216
+ dispatch = {
217
+ "id": _cmd_id,
218
+ "diff": _cmd_diff,
219
+ "verify": _cmd_verify,
220
+ "cat": _cmd_cat,
221
+ "version": _cmd_version,
222
+ }
223
+
224
+ return dispatch[args.command](args)
225
+
226
+
227
+ if __name__ == "__main__":
228
+ sys.exit(main())