orphograph 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the Orphograph contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: orphograph
3
+ Version: 0.1.0
4
+ Summary: Anchor folders to Bitcoin via the Orphograph hosted service. File contents stay on the local machine.
5
+ Author: the Orphograph contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 the Orphograph contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://orphograph.com
29
+ Project-URL: Documentation, https://orphograph.com/method/architecture.html
30
+ Keywords: bitcoin,opentimestamps,merkle,notarization,provenance
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3.9
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Security :: Cryptography
39
+ Classifier: Topic :: System :: Archiving
40
+ Requires-Python: >=3.9
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Dynamic: license-file
44
+
45
+ # orphograph
46
+
47
+ A Python SDK for anchoring a local folder to the Bitcoin chain through the
48
+ [Orphograph](https://orphograph.com) hosted service.
49
+
50
+ - License: MIT
51
+ - Python: 3.9 or newer
52
+ - Runtime dependencies: Python standard library only
53
+
54
+ ## Privacy contract
55
+
56
+ The library does not transmit file contents. For each file the SDK reads
57
+ the bytes locally, streams them through SHA-256 in one megabyte chunks,
58
+ and commits the digest into an RFC 6962 Merkle leaf bound to the file's
59
+ POSIX relative path. Only the resulting manifest (paths, per-file digests,
60
+ leaf hashes, and the 32-byte root) is sent across the network. The
61
+ verification path is symmetric: the root is recomputed locally from the
62
+ folder on disk and compared to the root recorded in the receipt.
63
+
64
+ The Merkle module is a verbatim copy of the server's reference
65
+ implementation, carrying the source SHA-256 in its header so divergence
66
+ from the canonical algorithm is immediately visible.
67
+
68
+ ## Install
69
+
70
+ ```
71
+ pip install orphograph
72
+ ```
73
+
74
+ ## Anchor a folder
75
+
76
+ ```python
77
+ from orphograph import anchor_folder
78
+
79
+ result = anchor_folder("/path/to/folder")
80
+ # {
81
+ # "receipt_id": "...",
82
+ # "root_hex": "...",
83
+ # "leaf_count": 42,
84
+ # "calendars_ok": 5,
85
+ # "calendars_total": 5,
86
+ # }
87
+ ```
88
+
89
+ Optional arguments:
90
+
91
+ | Argument | Purpose |
92
+ | -------------- | -------------------------------------------------------- |
93
+ | `server_url` | Base URL of the service (default `https://orphograph.com`). |
94
+ | `api_key` | Sent as `X-Orpho-Api-Key` when present. |
95
+ | `client_label` | Short free-form label persisted with the receipt. |
96
+ | `exclude` | Sequence of `fnmatch` patterns. `None` selects the default deny list (OS detritus, editor backups, build caches). Passing `[]` disables exclusion. |
97
+
98
+ ## Verify a folder
99
+
100
+ ```python
101
+ from orphograph import verify_folder
102
+
103
+ ok = verify_folder("/path/to/folder", receipt_id="...")
104
+ ```
105
+
106
+ The folder is walked locally, the Merkle root is recomputed, and the SDK
107
+ returns `True` only if the recomputed root equals the root recorded in
108
+ the receipt's manifest.
109
+
110
+ ## Inclusion proofs
111
+
112
+ A folder receipt can be queried for a proof that a single file belonged
113
+ to the anchored tree. The proof is verified locally; no further network
114
+ call is required to confirm it.
115
+
116
+ ```python
117
+ from orphograph import inclusion_proof, verify_inclusion
118
+
119
+ proof = inclusion_proof(receipt_id="...", path="sub/photo.jpg")
120
+ ok = verify_inclusion(
121
+ file_path="/path/to/sub/photo.jpg",
122
+ rel_path="sub/photo.jpg",
123
+ proof=proof["proof"],
124
+ root_hex=proof["root_hex"],
125
+ )
126
+ ```
127
+
128
+ ## Command line
129
+
130
+ The package installs an `orphograph` console script and is also runnable
131
+ as a module.
132
+
133
+ ```
134
+ python -m orphograph anchor /path/to/folder
135
+ python -m orphograph verify /path/to/folder <receipt_id>
136
+ python -m orphograph inclusion-proof <receipt_id> <posix/rel/path>
137
+ ```
138
+
139
+ Each subcommand writes a single line of JSON to standard output. The
140
+ `verify` subcommand exits with status `0` on a match and `1` on a
141
+ mismatch.
142
+
143
+ Environment variables:
144
+
145
+ | Variable | Purpose |
146
+ | ------------------ | --------------------------------------------- |
147
+ | `ORPHO_SERVER_URL` | Default base URL. |
148
+ | `ORPHO_API_KEY` | Default API key (sent as `X-Orpho-Api-Key`). |
149
+
150
+ ## Algorithm
151
+
152
+ The Merkle construction is RFC 6962 with a domain-separated leaf
153
+ (`0x00 || rel_path_utf8 || 0x00 || file_sha256`) and a domain-separated
154
+ internal node (`0x01 || left || right`). Odd-level remainders are
155
+ promoted, never duplicated, to avoid the second-preimage ambiguity of
156
+ the duplicate-last construction. The algorithm tag
157
+ `orphograph-merkle-v1-rfc6962` is embedded in every manifest.
158
+
159
+ ## License
160
+
161
+ MIT. See `LICENSE`.
@@ -0,0 +1,117 @@
1
+ # orphograph
2
+
3
+ A Python SDK for anchoring a local folder to the Bitcoin chain through the
4
+ [Orphograph](https://orphograph.com) hosted service.
5
+
6
+ - License: MIT
7
+ - Python: 3.9 or newer
8
+ - Runtime dependencies: Python standard library only
9
+
10
+ ## Privacy contract
11
+
12
+ The library does not transmit file contents. For each file the SDK reads
13
+ the bytes locally, streams them through SHA-256 in one megabyte chunks,
14
+ and commits the digest into an RFC 6962 Merkle leaf bound to the file's
15
+ POSIX relative path. Only the resulting manifest (paths, per-file digests,
16
+ leaf hashes, and the 32-byte root) is sent across the network. The
17
+ verification path is symmetric: the root is recomputed locally from the
18
+ folder on disk and compared to the root recorded in the receipt.
19
+
20
+ The Merkle module is a verbatim copy of the server's reference
21
+ implementation, carrying the source SHA-256 in its header so divergence
22
+ from the canonical algorithm is immediately visible.
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pip install orphograph
28
+ ```
29
+
30
+ ## Anchor a folder
31
+
32
+ ```python
33
+ from orphograph import anchor_folder
34
+
35
+ result = anchor_folder("/path/to/folder")
36
+ # {
37
+ # "receipt_id": "...",
38
+ # "root_hex": "...",
39
+ # "leaf_count": 42,
40
+ # "calendars_ok": 5,
41
+ # "calendars_total": 5,
42
+ # }
43
+ ```
44
+
45
+ Optional arguments:
46
+
47
+ | Argument | Purpose |
48
+ | -------------- | -------------------------------------------------------- |
49
+ | `server_url` | Base URL of the service (default `https://orphograph.com`). |
50
+ | `api_key` | Sent as `X-Orpho-Api-Key` when present. |
51
+ | `client_label` | Short free-form label persisted with the receipt. |
52
+ | `exclude` | Sequence of `fnmatch` patterns. `None` selects the default deny list (OS detritus, editor backups, build caches). Passing `[]` disables exclusion. |
53
+
54
+ ## Verify a folder
55
+
56
+ ```python
57
+ from orphograph import verify_folder
58
+
59
+ ok = verify_folder("/path/to/folder", receipt_id="...")
60
+ ```
61
+
62
+ The folder is walked locally, the Merkle root is recomputed, and the SDK
63
+ returns `True` only if the recomputed root equals the root recorded in
64
+ the receipt's manifest.
65
+
66
+ ## Inclusion proofs
67
+
68
+ A folder receipt can be queried for a proof that a single file belonged
69
+ to the anchored tree. The proof is verified locally; no further network
70
+ call is required to confirm it.
71
+
72
+ ```python
73
+ from orphograph import inclusion_proof, verify_inclusion
74
+
75
+ proof = inclusion_proof(receipt_id="...", path="sub/photo.jpg")
76
+ ok = verify_inclusion(
77
+ file_path="/path/to/sub/photo.jpg",
78
+ rel_path="sub/photo.jpg",
79
+ proof=proof["proof"],
80
+ root_hex=proof["root_hex"],
81
+ )
82
+ ```
83
+
84
+ ## Command line
85
+
86
+ The package installs an `orphograph` console script and is also runnable
87
+ as a module.
88
+
89
+ ```
90
+ python -m orphograph anchor /path/to/folder
91
+ python -m orphograph verify /path/to/folder <receipt_id>
92
+ python -m orphograph inclusion-proof <receipt_id> <posix/rel/path>
93
+ ```
94
+
95
+ Each subcommand writes a single line of JSON to standard output. The
96
+ `verify` subcommand exits with status `0` on a match and `1` on a
97
+ mismatch.
98
+
99
+ Environment variables:
100
+
101
+ | Variable | Purpose |
102
+ | ------------------ | --------------------------------------------- |
103
+ | `ORPHO_SERVER_URL` | Default base URL. |
104
+ | `ORPHO_API_KEY` | Default API key (sent as `X-Orpho-Api-Key`). |
105
+
106
+ ## Algorithm
107
+
108
+ The Merkle construction is RFC 6962 with a domain-separated leaf
109
+ (`0x00 || rel_path_utf8 || 0x00 || file_sha256`) and a domain-separated
110
+ internal node (`0x01 || left || right`). Odd-level remainders are
111
+ promoted, never duplicated, to avoid the second-preimage ambiguity of
112
+ the duplicate-last construction. The algorithm tag
113
+ `orphograph-merkle-v1-rfc6962` is embedded in every manifest.
114
+
115
+ ## License
116
+
117
+ MIT. See `LICENSE`.
@@ -0,0 +1,175 @@
1
+ """orphograph — Python SDK for anchoring folders to Bitcoin.
2
+
3
+ The SDK constructs an RFC 6962-style Merkle tree from a local folder, then
4
+ submits only the manifest (paths, per-file SHA-256 digests, leaf hashes,
5
+ and a 32-byte root) to the Orphograph hosted service. File bodies do not
6
+ cross the network at any point in this module.
7
+
8
+ Public API:
9
+
10
+ anchor_folder(folder_path, server_url=..., api_key=..., client_label=..., exclude=...)
11
+ verify_folder(folder_path, receipt_id, server_url=...)
12
+ inclusion_proof(receipt_id, path, server_url=...)
13
+ verify_inclusion(file_path, rel_path, proof, root_hex)
14
+
15
+ Algorithm tag: ``orphograph-merkle-v1-rfc6962``.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ from pathlib import Path
21
+ from typing import Optional, Sequence
22
+
23
+ from . import _client
24
+ from ._client import OrphographError
25
+ from ._merkle import ALGORITHM, MerkleTree
26
+
27
+ __all__ = [
28
+ "ALGORITHM",
29
+ "OrphographError",
30
+ "anchor_folder",
31
+ "verify_folder",
32
+ "inclusion_proof",
33
+ "verify_inclusion",
34
+ ]
35
+
36
+ __version__ = "0.1.0"
37
+
38
+
39
+ def anchor_folder(
40
+ folder_path: str,
41
+ *,
42
+ server_url: str = _client.DEFAULT_SERVER_URL,
43
+ api_key: Optional[str] = None,
44
+ client_label: Optional[str] = None,
45
+ exclude: Optional[Sequence[str]] = None,
46
+ ) -> dict:
47
+ """Anchor a folder to Bitcoin via the Orphograph hosted service.
48
+
49
+ The Merkle tree is constructed locally; only the resulting manifest
50
+ (paths + SHA-256 digests + leaf hashes + root) is transmitted to the
51
+ server. File contents stay on disk.
52
+
53
+ Returns a dict with keys: ``receipt_id``, ``root_hex``, ``leaf_count``,
54
+ ``calendars_ok``, ``calendars_total``.
55
+ """
56
+ root = Path(folder_path)
57
+ if not root.is_dir():
58
+ raise ValueError(f"not a directory: {folder_path}")
59
+ excl_list = list(exclude) if exclude is not None else None
60
+ tree = MerkleTree.from_folder(root, exclude=excl_list)
61
+ manifest = tree.manifest()
62
+ response = _client.post_anchor_folder(
63
+ manifest,
64
+ server_url=server_url,
65
+ api_key=api_key,
66
+ client_label=client_label,
67
+ )
68
+ return {
69
+ "receipt_id": response.get("receipt_id"),
70
+ "root_hex": response.get("root_hex", tree.root_hex()),
71
+ "leaf_count": response.get("leaf_count", len(manifest["leaves"])),
72
+ "calendars_ok": response.get("calendars_ok"),
73
+ "calendars_total": response.get("calendars_total"),
74
+ }
75
+
76
+
77
+ def verify_folder(
78
+ folder_path: str,
79
+ receipt_id: str,
80
+ *,
81
+ server_url: str = _client.DEFAULT_SERVER_URL,
82
+ api_key: Optional[str] = None,
83
+ exclude: Optional[list] = None,
84
+ ) -> bool:
85
+ """Verify a local folder against a previously anchored receipt.
86
+
87
+ Rebuilds the Merkle root locally from the folder on disk and compares
88
+ it byte-for-byte to the root recorded in the receipt's manifest. The
89
+ server is consulted only to fetch the manifest; file contents are not
90
+ transmitted.
91
+ """
92
+ root = Path(folder_path)
93
+ if not root.is_dir():
94
+ raise ValueError(f"not a directory: {folder_path}")
95
+ response = _client.get_verify_folder(receipt_id, server_url=server_url, api_key=api_key)
96
+ manifest = response.get("manifest") or {}
97
+ server_root = manifest.get("root_hex")
98
+ if not server_root:
99
+ return False
100
+ # The manifest is authoritative for its own scope.
101
+ #
102
+ # Previously this mirrored whatever the CALLER passed, which only moved the
103
+ # problem: a folder anchored with custom excludes still could not verify
104
+ # unless the verifier independently remembered the exact list used at
105
+ # capture, months earlier, possibly on another machine. That is not a
106
+ # property evidence should have.
107
+ #
108
+ # Manifests now record the effective patterns (see merkle.build_scope), so
109
+ # they are read from the manifest when present. The caller's `exclude` is
110
+ # only a fallback for manifests issued before scope existed.
111
+ effective_exclude = exclude
112
+ scope = manifest.get("scope")
113
+ if isinstance(scope, dict) and isinstance(scope.get("exclude"), list):
114
+ effective_exclude = list(scope["exclude"])
115
+ tree = MerkleTree.from_folder(root, exclude=effective_exclude)
116
+ return tree.root_hex() == server_root
117
+
118
+
119
+ def inclusion_proof(
120
+ receipt_id: str,
121
+ path: str,
122
+ *,
123
+ server_url: str = _client.DEFAULT_SERVER_URL,
124
+ api_key: Optional[str] = None,
125
+ ) -> dict:
126
+ """Fetch an inclusion proof for one POSIX-relative path in a folder receipt.
127
+
128
+ Returns the server's JSON payload: ``receipt_id``, ``root_hex``,
129
+ ``path``, ``file_sha256_hex``, ``proof`` (list of ``[direction, hex]``).
130
+ """
131
+ return _client.get_inclusion_proof(
132
+ receipt_id, path, server_url=server_url, api_key=api_key
133
+ )
134
+
135
+
136
+ def verify_inclusion(
137
+ file_path: str,
138
+ rel_path: str,
139
+ proof: Sequence,
140
+ root_hex: str,
141
+ ) -> bool:
142
+ """Verify locally that a file was included in an anchored folder.
143
+
144
+ Reads the file from disk, computes its SHA-256, and walks the proof
145
+ upward against the supplied root. No network call is made.
146
+
147
+ Raises ``FileNotFoundError`` when ``file_path`` does not exist (or is
148
+ not a regular file). A missing local file is an I/O precondition
149
+ failure, not a "not included" verdict — for a notary, a distinguishable
150
+ error beats a silent ``False`` (audit D7; matches the Node SDK, whose
151
+ ``verifyInclusion`` rejects with the filesystem error). A malformed
152
+ proof or root still returns ``False`` per VERIFIER_SPEC §4.1.
153
+ """
154
+ p = Path(file_path)
155
+ if not p.is_file():
156
+ raise FileNotFoundError(f"no such file: {file_path}")
157
+ h = hashlib.sha256()
158
+ with p.open("rb") as f:
159
+ while True:
160
+ chunk = f.read(1024 * 1024)
161
+ if not chunk:
162
+ break
163
+ h.update(chunk)
164
+ file_hash = h.digest()
165
+ try:
166
+ root = bytes.fromhex(root_hex)
167
+ except (ValueError, TypeError):
168
+ return False
169
+ # Coerce proof entries (which may arrive as lists from JSON) to tuples.
170
+ normalised: list = []
171
+ for step in proof:
172
+ if not isinstance(step, (list, tuple)) or len(step) != 2:
173
+ return False
174
+ normalised.append((step[0], step[1]))
175
+ return MerkleTree.verify_inclusion(file_hash, rel_path, normalised, root)
@@ -0,0 +1,5 @@
1
+ """Entry point for ``python -m orphograph``."""
2
+ from ._cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,129 @@
1
+ """orphograph._cli — argparse-based command-line interface.
2
+
3
+ Subcommands:
4
+
5
+ anchor <folder> Anchor a folder; prints one line of JSON.
6
+ verify <folder> <receipt_id> Verify a folder against a receipt.
7
+ inclusion-proof <rid> <path> Fetch an inclusion proof; prints JSON.
8
+
9
+ Both ``anchor`` and ``verify`` accept repeatable ``--exclude GLOB`` flags.
10
+ A folder anchored with custom excludes can only re-derive the same Merkle
11
+ root when verified with the SAME excludes (AUDIT_VERIFIER_DRIFT D2). Since
12
+ Wedge 01 the manifest records them in its ``scope`` block and ``verify``
13
+ reads them from there (the manifest is authoritative — VERIFIER_SPEC §4.2);
14
+ the ``--exclude`` flag on ``verify`` only applies to manifests that carry no
15
+ scope block (issued before scope existed).
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import sys
23
+ from typing import List, Optional
24
+
25
+ from . import anchor_folder, inclusion_proof, verify_folder
26
+ from ._client import DEFAULT_SERVER_URL, OrphographError
27
+
28
+
29
+ def _env_server() -> str:
30
+ return os.environ.get("ORPHO_SERVER_URL", DEFAULT_SERVER_URL)
31
+
32
+
33
+ def _env_api_key() -> Optional[str]:
34
+ val = os.environ.get("ORPHO_API_KEY", "").strip()
35
+ return val or None
36
+
37
+
38
+ def _build_parser() -> argparse.ArgumentParser:
39
+ parser = argparse.ArgumentParser(
40
+ prog="orphograph",
41
+ description="Anchor folders to Bitcoin via the Orphograph service.",
42
+ )
43
+ parser.add_argument(
44
+ "--server-url",
45
+ default=None,
46
+ help="Base URL of the Orphograph service (default: ORPHO_SERVER_URL or https://orphograph.com).",
47
+ )
48
+ parser.add_argument(
49
+ "--api-key",
50
+ default=None,
51
+ help="Optional API key (default: ORPHO_API_KEY).",
52
+ )
53
+ sub = parser.add_subparsers(dest="command", required=True)
54
+
55
+ exclude_help = (
56
+ "Glob pattern to exclude (repeatable). Supplying any --exclude "
57
+ "REPLACES the default deny-list rather than extending it. "
58
+ "On verify: applies only to manifests without a scope block — a "
59
+ "manifest's recorded scope.exclude is authoritative (VERIFIER_SPEC §4.2)."
60
+ )
61
+
62
+ p_anchor = sub.add_parser("anchor", help="Anchor a folder.")
63
+ p_anchor.add_argument("folder", help="Local folder to anchor.")
64
+ p_anchor.add_argument("--label", default=None, help="Optional short client label.")
65
+ p_anchor.add_argument(
66
+ "--exclude", action="append", default=None, metavar="GLOB", help=exclude_help
67
+ )
68
+
69
+ p_verify = sub.add_parser("verify", help="Verify a folder against a receipt.")
70
+ p_verify.add_argument("folder", help="Local folder to verify.")
71
+ p_verify.add_argument("receipt_id", help="Receipt id returned at anchor time.")
72
+ p_verify.add_argument(
73
+ "--exclude", action="append", default=None, metavar="GLOB", help=exclude_help
74
+ )
75
+
76
+ p_proof = sub.add_parser("inclusion-proof", help="Fetch an inclusion proof.")
77
+ p_proof.add_argument("receipt_id", help="Folder receipt id.")
78
+ p_proof.add_argument("path", help="POSIX relative path inside the folder.")
79
+
80
+ return parser
81
+
82
+
83
+ def main(argv: Optional[List[str]] = None) -> int:
84
+ parser = _build_parser()
85
+ args = parser.parse_args(argv)
86
+ server_url = args.server_url or _env_server()
87
+ api_key = args.api_key if args.api_key is not None else _env_api_key()
88
+
89
+ try:
90
+ if args.command == "anchor":
91
+ result = anchor_folder(
92
+ args.folder,
93
+ server_url=server_url,
94
+ api_key=api_key,
95
+ client_label=args.label,
96
+ exclude=args.exclude,
97
+ )
98
+ sys.stdout.write(json.dumps(result) + "\n")
99
+ return 0
100
+ if args.command == "verify":
101
+ ok = verify_folder(
102
+ args.folder,
103
+ args.receipt_id,
104
+ server_url=server_url,
105
+ api_key=api_key,
106
+ exclude=args.exclude,
107
+ )
108
+ sys.stdout.write(json.dumps({"match": bool(ok)}) + "\n")
109
+ return 0 if ok else 1
110
+ if args.command == "inclusion-proof":
111
+ proof = inclusion_proof(
112
+ args.receipt_id,
113
+ args.path,
114
+ server_url=server_url,
115
+ api_key=api_key,
116
+ )
117
+ sys.stdout.write(json.dumps(proof) + "\n")
118
+ return 0
119
+ except OrphographError as e:
120
+ sys.stderr.write(json.dumps({"error": e.message, "status": e.status}) + "\n")
121
+ return 2
122
+ except (OSError, ValueError) as e:
123
+ sys.stderr.write(json.dumps({"error": str(e)}) + "\n")
124
+ return 2
125
+ return 2
126
+
127
+
128
+ if __name__ == "__main__":
129
+ raise SystemExit(main())