mirrorcommons 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.
@@ -0,0 +1,3 @@
1
+ """ArtifactGrid continuity manifest tools."""
2
+
3
+ __version__ = "0.1.0"
artifactgrid/cli.py ADDED
@@ -0,0 +1,380 @@
1
+ """Command-line interface for the local ArtifactGrid validation workflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from .manifest import (
11
+ build_manifest,
12
+ build_publisher_mirror_manifest,
13
+ protect_snapshot,
14
+ protect_snapshot_to_s3,
15
+ verify_manifest,
16
+ verify_s3_manifest,
17
+ )
18
+ from .continuity import (
19
+ check_huggingface_source,
20
+ discard_temporary_snapshot,
21
+ download_huggingface_snapshot,
22
+ restore_s3_snapshot,
23
+ restore_snapshot,
24
+ write_private_continuity_report,
25
+ write_private_s3_continuity_report,
26
+ )
27
+ from .publisher_api import complete_upload, create_publisher_draft, create_upload_session, decide_review, issue_source_challenge, publish_mirror, publisher_draft_status, publisher_token, put_multipart_file, put_staged_file, refresh_upload_grant, tombstone_mirror, verify_source_challenge
28
+
29
+
30
+ def _add_snapshot_arguments(parser: argparse.ArgumentParser) -> None:
31
+ parser.add_argument("--source-url", required=True, help="Public Hugging Face model URL.")
32
+ parser.add_argument("--revision", required=True, help="Full 40-character source commit hash.")
33
+ source = parser.add_mutually_exclusive_group(required=True)
34
+ source.add_argument("--snapshot-dir", type=Path, help="Existing local snapshot directory.")
35
+ source.add_argument("--download", action="store_true", help="Download the exact revision directly from Hugging Face.")
36
+
37
+
38
+ def _snapshot_directory(args: argparse.Namespace) -> Path:
39
+ if args.download:
40
+ return download_huggingface_snapshot(args.source_url, args.revision)
41
+ return args.snapshot_dir
42
+
43
+
44
+ def _parser() -> argparse.ArgumentParser:
45
+ parser = argparse.ArgumentParser(prog="artifactgrid", description="Create and verify local continuity evidence.")
46
+ subcommands = parser.add_subparsers(dest="command", required=True)
47
+
48
+ inspect_parser = subcommands.add_parser("inspect", help="Build a manifest from an existing local snapshot.")
49
+ _add_snapshot_arguments(inspect_parser)
50
+ inspect_parser.add_argument("--output", type=Path, help="Optional path for the JSON manifest. Prints to stdout when omitted.")
51
+
52
+ source_check_parser = subcommands.add_parser("source-check", help="Check whether the exact public source revision is reachable.")
53
+ source_check_parser.add_argument("--source-url", required=True, help="Public Hugging Face model URL.")
54
+ source_check_parser.add_argument("--revision", required=True, help="Full 40-character source commit hash.")
55
+ source_check_parser.add_argument("--output", type=Path, help="Optional path for the JSON observation. Prints to stdout when omitted.")
56
+
57
+ protect_parser = subcommands.add_parser("protect", help="Copy a supported snapshot into customer-controlled local storage.")
58
+ _add_snapshot_arguments(protect_parser)
59
+ protect_target = protect_parser.add_mutually_exclusive_group(required=True)
60
+ protect_target.add_argument("--backup-dir", type=Path, help="Customer-controlled local destination directory.")
61
+ protect_target.add_argument("--s3-bucket", help="Customer-controlled S3-compatible bucket.")
62
+ protect_parser.add_argument("--s3-prefix", default="artifactgrid", help="S3 object prefix when using --s3-bucket.")
63
+ protect_parser.add_argument("--s3-endpoint-url", help="Optional S3-compatible endpoint, for example an R2 endpoint.")
64
+ protect_parser.add_argument("--output", type=Path, help="Optional path for a portable local copy of the manifest.")
65
+
66
+ verify_parser = subcommands.add_parser("verify", help="Verify a previously protected local backup.")
67
+ verify_parser.add_argument("--manifest", type=Path, required=True, help="Path to artifactgrid-manifest.json.")
68
+ verify_target = verify_parser.add_mutually_exclusive_group(required=True)
69
+ verify_target.add_argument("--backup-dir", type=Path, help="Directory containing the protected files.")
70
+ verify_target.add_argument("--s3-bucket", help="Customer-controlled S3-compatible bucket.")
71
+ verify_parser.add_argument("--s3-prefix", default="artifactgrid", help="S3 object prefix when using --s3-bucket.")
72
+ verify_parser.add_argument("--s3-endpoint-url", help="Optional S3-compatible endpoint, for example an R2 endpoint.")
73
+
74
+ restore_parser = subcommands.add_parser("restore", help="Run a clean restore drill from a verified customer backup.")
75
+ restore_parser.add_argument("--manifest", type=Path, required=True, help="Path to artifactgrid-manifest.json.")
76
+ restore_target = restore_parser.add_mutually_exclusive_group(required=True)
77
+ restore_target.add_argument("--backup-dir", type=Path, help="Verified customer-controlled local backup directory.")
78
+ restore_target.add_argument("--s3-bucket", help="Verified customer-controlled S3-compatible bucket.")
79
+ restore_parser.add_argument("--s3-prefix", default="artifactgrid", help="S3 object prefix when using --s3-bucket.")
80
+ restore_parser.add_argument("--s3-endpoint-url", help="Optional S3-compatible endpoint, for example an R2 endpoint.")
81
+ restore_parser.add_argument("--restore-dir", type=Path, required=True, help="Empty destination for the restore drill.")
82
+
83
+ report_parser = subcommands.add_parser("report", help="Write a private Consumer Continuity Report.")
84
+ report_parser.add_argument("--manifest", type=Path, required=True, help="Path to artifactgrid-manifest.json.")
85
+ report_target = report_parser.add_mutually_exclusive_group(required=True)
86
+ report_target.add_argument("--backup-dir", type=Path, help="Verified customer-controlled local backup directory.")
87
+ report_target.add_argument("--s3-bucket", help="Verified customer-controlled S3-compatible bucket.")
88
+ report_parser.add_argument("--s3-prefix", default="artifactgrid", help="S3 object prefix when using --s3-bucket.")
89
+ report_parser.add_argument("--s3-endpoint-url", help="Optional S3-compatible endpoint, for example an R2 endpoint.")
90
+ report_parser.add_argument("--output", type=Path, required=True, help="Destination for the private HTML report.")
91
+ report_parser.add_argument("--restore-result", type=Path, help="Optional artifactgrid-restore-result.json from a drill.")
92
+ report_parser.add_argument("--source-observation", type=Path, help="Optional source-check JSON for this exact pinned revision.")
93
+
94
+ publish_parser = subcommands.add_parser("publish", help="Create and manage a Commons publisher draft through the shared control plane.")
95
+ publish_commands = publish_parser.add_subparsers(dest="publish_command", required=True)
96
+ publish_create = publish_commands.add_parser("create", help="Create a staged publisher draft; no files are uploaded by this command.")
97
+ publish_create.add_argument("--slug", required=True, help="Public ArtifactGrid Commons slug.")
98
+ publish_create.add_argument("--display-name", required=True, help="Public display name.")
99
+ publish_create.add_argument("--declared-license", required=True, help="License identifier or exact declared license text.")
100
+ publish_create.add_argument("--origin", choices=("community_mirror", "source_mirror", "original_creator_submission"), required=True)
101
+ publish_create.add_argument("--source-url", help="Public Hugging Face source URL for a community or source mirror.")
102
+ publish_create.add_argument("--revision", help="Full 40-character source commit for a community or source mirror.")
103
+ publish_create.add_argument("--api-url", help="Override the ArtifactGrid API URL; for local development only.")
104
+ publish_status = publish_commands.add_parser("status", help="Read the state of one of your private publisher drafts.")
105
+ publish_status.add_argument("--draft", required=True, help="Draft ID returned by publish create.")
106
+ publish_status.add_argument("--api-url", help="Override the ArtifactGrid API URL; for local development only.")
107
+ publish_challenge = publish_commands.add_parser("source-challenge", help="Issue a repository-controller challenge for a source mirror.")
108
+ publish_challenge.add_argument("--draft", required=True, help="Draft ID returned by publish create.")
109
+ publish_challenge.add_argument("--api-url", help="Override the ArtifactGrid API URL; for local development only.")
110
+ publish_verify_source = publish_commands.add_parser("verify-source", help="Check the controller challenge at the pinned source revision.")
111
+ publish_verify_source.add_argument("--draft", required=True, help="Draft ID returned by publish create.")
112
+ publish_verify_source.add_argument("--api-url", help="Override the ArtifactGrid API URL; for local development only.")
113
+ publish_upload = publish_commands.add_parser("upload", help="Upload a declared publisher manifest through private R2 staging grants.")
114
+ publish_upload.add_argument("--draft", required=True, help="Draft ID returned by publish create.")
115
+ publish_upload.add_argument("--manifest", type=Path, required=True, help="Publisher mirror manifest JSON created for this exact snapshot.")
116
+ publish_upload.add_argument("--snapshot-dir", type=Path, required=True, help="Directory containing the declared files.")
117
+ publish_upload.add_argument("--api-url", help="Override the ArtifactGrid API URL; for local development only.")
118
+ publish_inspect = publish_commands.add_parser("inspect", help="Build the local file inventory required for a publisher upload.")
119
+ publish_inspect.add_argument("--snapshot-dir", type=Path, required=True, help="Directory containing the staged artifact files.")
120
+ publish_inspect.add_argument("--slug", required=True, help="Public ArtifactGrid Commons slug.")
121
+ publish_inspect.add_argument("--origin", choices=("community_mirror", "source_mirror", "original_creator_submission"), required=True)
122
+ publish_inspect.add_argument("--source-url", help="Public Hugging Face source URL for a community or source mirror.")
123
+ publish_inspect.add_argument("--revision", help="Full 40-character source commit for a community or source mirror.")
124
+ publish_inspect.add_argument("--source-control", choices=("official_source_controller_verified", "not_claimed", "not_applicable"), required=True)
125
+ publish_inspect.add_argument("--output", type=Path, required=True, help="Destination for publisher-mirror-manifest.json.")
126
+
127
+ review_parser = subcommands.add_parser("review", help="Reviewer-only ArtifactGrid Commons actions.")
128
+ review_commands = review_parser.add_subparsers(dest="review_command", required=True)
129
+ review_decision = review_commands.add_parser("decision", help="Approve, restrict, or reject a private review case.")
130
+ review_decision.add_argument("--case", required=True, help="Private review case ID.")
131
+ review_decision.add_argument("--decision", choices=("approved", "restricted", "rejected"), required=True)
132
+ review_decision.add_argument("--api-url", help="Override the ArtifactGrid API URL; for local development only.")
133
+ review_publish = review_commands.add_parser("publish", help="Publish an approved mirror into the public Commons prefix.")
134
+ review_publish.add_argument("--mirror", required=True, help="Approved mirror ID.")
135
+ review_publish.add_argument("--api-url", help="Override the ArtifactGrid API URL; for local development only.")
136
+ review_tombstone = review_commands.add_parser("tombstone", help="Remove a published mirror while retaining an operator audit record.")
137
+ review_tombstone.add_argument("--mirror", required=True, help="Published mirror ID.")
138
+ review_tombstone.add_argument("--reason", required=True, help="Bounded operator reason retained in the private review history.")
139
+ review_tombstone.add_argument("--api-url", help="Override the ArtifactGrid API URL; for local development only.")
140
+ return parser
141
+
142
+
143
+ def _s3_client(endpoint_url: str | None):
144
+ try:
145
+ import boto3
146
+ except ImportError as error:
147
+ raise RuntimeError("S3 support requires the boto3 dependency. Reinstall ArtifactGrid with its project dependencies.") from error
148
+ return boto3.client("s3", endpoint_url=endpoint_url)
149
+
150
+
151
+ def _write_or_print_manifest(manifest: dict, output: Path | None) -> None:
152
+ serialized = json.dumps(manifest, indent=2, sort_keys=True) + "\n"
153
+ if output:
154
+ destination = output.expanduser()
155
+ destination.parent.mkdir(parents=True, exist_ok=True)
156
+ destination.write_text(serialized, encoding="utf-8")
157
+ else:
158
+ sys.stdout.write(serialized)
159
+
160
+
161
+ def main(argv: list[str] | None = None) -> int:
162
+ args = _parser().parse_args(argv)
163
+ try:
164
+ if args.command == "review" and args.review_command == "decision":
165
+ _write_or_print_manifest(decide_review(token=publisher_token(), review_case_id=args.case, decision=args.decision, base_url=args.api_url), None)
166
+ return 0
167
+ if args.command == "review" and args.review_command == "publish":
168
+ _write_or_print_manifest(publish_mirror(token=publisher_token(), mirror_id=args.mirror, base_url=args.api_url), None)
169
+ return 0
170
+ if args.command == "review" and args.review_command == "tombstone":
171
+ _write_or_print_manifest(tombstone_mirror(token=publisher_token(), mirror_id=args.mirror, reason=args.reason, base_url=args.api_url), None)
172
+ return 0
173
+ if args.command == "publish" and args.publish_command == "create":
174
+ response = create_publisher_draft(
175
+ token=publisher_token(),
176
+ slug=args.slug,
177
+ display_name=args.display_name,
178
+ declared_license=args.declared_license,
179
+ origin=args.origin,
180
+ source_url=args.source_url,
181
+ source_revision=args.revision,
182
+ base_url=args.api_url,
183
+ )
184
+ _write_or_print_manifest(response, None)
185
+ return 0
186
+ if args.command == "publish" and args.publish_command == "status":
187
+ _write_or_print_manifest(
188
+ publisher_draft_status(token=publisher_token(), draft_id=args.draft, base_url=args.api_url), None
189
+ )
190
+ return 0
191
+ if args.command == "publish" and args.publish_command == "source-challenge":
192
+ _write_or_print_manifest(
193
+ issue_source_challenge(token=publisher_token(), draft_id=args.draft, base_url=args.api_url), None
194
+ )
195
+ return 0
196
+ if args.command == "publish" and args.publish_command == "verify-source":
197
+ _write_or_print_manifest(
198
+ verify_source_challenge(token=publisher_token(), draft_id=args.draft, base_url=args.api_url), None
199
+ )
200
+ return 0
201
+ if args.command == "publish" and args.publish_command == "upload":
202
+ manifest = json.loads(args.manifest.expanduser().read_text(encoding="utf-8"))
203
+ if not isinstance(manifest, dict) or manifest.get("spec") != "artifactgrid/publisher-mirror-manifest@0.1":
204
+ raise ValueError("manifest must be an ArtifactGrid publisher mirror manifest")
205
+ files = manifest.get("files")
206
+ if not isinstance(files, list) or not files:
207
+ raise ValueError("publisher manifest contains no files")
208
+ declared_files: list[dict[str, object]] = []
209
+ root = args.snapshot_dir.expanduser().resolve()
210
+ for entry in files:
211
+ if not isinstance(entry, dict) or not isinstance(entry.get("path"), str) or not isinstance(entry.get("bytes"), int) or not isinstance(entry.get("sha256"), str):
212
+ raise ValueError("publisher manifest contains an invalid file entry")
213
+ declared_files.append({"path": entry["path"], "bytes": entry["bytes"], "sha256": entry["sha256"]})
214
+ session = create_upload_session(token=publisher_token(), draft_id=args.draft, files=declared_files, base_url=args.api_url)
215
+ upload = session.get("upload") if isinstance(session, dict) else None
216
+ grants = upload.get("files") if isinstance(upload, dict) else None
217
+ if not isinstance(grants, list) or len(grants) != len(declared_files):
218
+ raise RuntimeError("ArtifactGrid returned an invalid upload session")
219
+ upload_id = upload.get("id")
220
+ if not isinstance(upload_id, str):
221
+ raise RuntimeError("ArtifactGrid returned an upload without an ID")
222
+
223
+ def fresh_single_url(path: str) -> str:
224
+ grant = refresh_upload_grant(token=publisher_token(), draft_id=args.draft, upload_id=upload_id, path=path, base_url=args.api_url)
225
+ put_url = grant.get("grant", {}).get("putUrl")
226
+ if not isinstance(put_url, str):
227
+ raise RuntimeError("ArtifactGrid returned an invalid refreshed upload grant")
228
+ return put_url
229
+
230
+ def fresh_part_url(path: str, part_number: int) -> str:
231
+ grant = refresh_upload_grant(token=publisher_token(), draft_id=args.draft, upload_id=upload_id, path=path, part_numbers=[part_number], base_url=args.api_url)
232
+ parts = grant.get("grant", {}).get("parts")
233
+ if not isinstance(parts, list) or not parts or not isinstance(parts[0].get("putUrl"), str):
234
+ raise RuntimeError("ArtifactGrid returned an invalid refreshed multipart grant")
235
+ return parts[0]["putUrl"]
236
+
237
+ multipart_completions: list[dict[str, object]] = []
238
+ for entry, grant in zip(declared_files, grants, strict=True):
239
+ if not isinstance(grant, dict):
240
+ raise RuntimeError("ArtifactGrid returned an invalid file upload grant")
241
+ path = str(entry["path"])
242
+ source = root / path
243
+ if grant.get("kind") == "multipart":
244
+ parts = grant.get("parts")
245
+ if not isinstance(parts, list):
246
+ raise RuntimeError("ArtifactGrid returned an invalid multipart upload grant")
247
+ completions = put_multipart_file(
248
+ parts=parts, source=source, expected_bytes=int(entry["bytes"]),
249
+ get_fresh_part_url=lambda part_number, path=path: fresh_part_url(path, part_number),
250
+ )
251
+ multipart_completions.append({"path": path, "parts": completions})
252
+ elif isinstance(grant.get("putUrl"), str):
253
+ put_staged_file(
254
+ put_url=grant["putUrl"], source=source, expected_bytes=int(entry["bytes"]),
255
+ get_fresh_put_url=lambda path=path: fresh_single_url(path),
256
+ )
257
+ else:
258
+ raise RuntimeError("ArtifactGrid returned an invalid file upload grant")
259
+ completion = complete_upload(token=publisher_token(), draft_id=args.draft, upload_id=upload_id, multipart=multipart_completions or None, base_url=args.api_url)
260
+ _write_or_print_manifest(completion, None)
261
+ return 0
262
+ if args.command == "publish" and args.publish_command == "inspect":
263
+ _write_or_print_manifest(
264
+ build_publisher_mirror_manifest(
265
+ args.snapshot_dir,
266
+ slug=args.slug,
267
+ origin=args.origin,
268
+ source_control=args.source_control,
269
+ source_url=args.source_url,
270
+ source_revision=args.revision,
271
+ ),
272
+ args.output,
273
+ )
274
+ return 0
275
+ if args.command == "source-check":
276
+ _write_or_print_manifest(check_huggingface_source(args.source_url, args.revision), args.output)
277
+ return 0
278
+ if args.command == "inspect":
279
+ snapshot_directory = _snapshot_directory(args)
280
+ try:
281
+ manifest = build_manifest(snapshot_directory, args.source_url, args.revision)
282
+ _write_or_print_manifest(manifest, args.output)
283
+ return 0
284
+ finally:
285
+ if args.download:
286
+ discard_temporary_snapshot(snapshot_directory)
287
+ if args.command == "protect":
288
+ snapshot_directory = _snapshot_directory(args)
289
+ try:
290
+ if args.s3_bucket:
291
+ manifest = protect_snapshot_to_s3(
292
+ snapshot_directory,
293
+ _s3_client(args.s3_endpoint_url),
294
+ args.s3_bucket,
295
+ args.s3_prefix,
296
+ args.source_url,
297
+ args.revision,
298
+ )
299
+ _write_or_print_manifest(manifest, args.output)
300
+ return 0
301
+ manifest_path = protect_snapshot(snapshot_directory, args.backup_dir, args.source_url, args.revision)
302
+ if args.output:
303
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
304
+ if not isinstance(manifest, dict):
305
+ raise RuntimeError("local backup wrote an invalid manifest")
306
+ _write_or_print_manifest(manifest, args.output)
307
+ print(f"Customer-attested backup written to {manifest_path}")
308
+ return 0
309
+ finally:
310
+ if args.download:
311
+ discard_temporary_snapshot(snapshot_directory)
312
+ if args.command == "restore":
313
+ if args.s3_bucket:
314
+ manifest = json.loads(args.manifest.expanduser().read_text(encoding="utf-8"))
315
+ if not isinstance(manifest, dict):
316
+ raise ValueError("manifest must be a JSON object")
317
+ result = restore_s3_snapshot(
318
+ manifest,
319
+ _s3_client(args.s3_endpoint_url),
320
+ args.s3_bucket,
321
+ args.s3_prefix,
322
+ args.restore_dir,
323
+ )
324
+ else:
325
+ result = restore_snapshot(args.manifest, args.backup_dir, args.restore_dir)
326
+ print(f"Restore drill passed in {result['recovery_time_ms']} ms. Evidence written to {args.restore_dir}")
327
+ return 0
328
+ if args.command == "report":
329
+ restore_result = None
330
+ if args.restore_result:
331
+ restore_result = json.loads(args.restore_result.expanduser().read_text(encoding="utf-8"))
332
+ if not isinstance(restore_result, dict):
333
+ raise ValueError("restore result must be a JSON object")
334
+ source_observation = None
335
+ if args.source_observation:
336
+ source_observation = json.loads(args.source_observation.expanduser().read_text(encoding="utf-8"))
337
+ if not isinstance(source_observation, dict):
338
+ raise ValueError("source observation must be a JSON object")
339
+ if args.s3_bucket:
340
+ manifest = json.loads(args.manifest.expanduser().read_text(encoding="utf-8"))
341
+ if not isinstance(manifest, dict):
342
+ raise ValueError("manifest must be a JSON object")
343
+ report_path = write_private_s3_continuity_report(
344
+ manifest,
345
+ _s3_client(args.s3_endpoint_url),
346
+ args.s3_bucket,
347
+ args.s3_prefix,
348
+ report_path=args.output,
349
+ restore_result=restore_result,
350
+ source_observation=source_observation,
351
+ )
352
+ else:
353
+ report_path = write_private_continuity_report(
354
+ args.manifest,
355
+ args.backup_dir,
356
+ report_path=args.output,
357
+ restore_result=restore_result,
358
+ source_observation=source_observation,
359
+ )
360
+ print(f"Private Consumer Continuity Report written to {report_path}")
361
+ return 0
362
+ if args.s3_bucket:
363
+ manifest = json.loads(args.manifest.expanduser().read_text(encoding="utf-8"))
364
+ errors = verify_s3_manifest(manifest, _s3_client(args.s3_endpoint_url), args.s3_bucket, args.s3_prefix)
365
+ else:
366
+ errors = verify_manifest(args.manifest, args.backup_dir)
367
+ if errors:
368
+ print("Verification failed:", file=sys.stderr)
369
+ for error in errors:
370
+ print(f"- {error}", file=sys.stderr)
371
+ return 1
372
+ print("Verification passed: every declared file exists and matches its SHA-256 checksum.")
373
+ return 0
374
+ except (OSError, RuntimeError, ValueError) as error:
375
+ print(f"ArtifactGrid: {error}", file=sys.stderr)
376
+ return 2
377
+
378
+
379
+ if __name__ == "__main__":
380
+ raise SystemExit(main())