trustbeat 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,50 @@
1
+ # Publishes trustbeat to PyPI on a version tag.
2
+ #
3
+ # This workflow lives at the root of TrustBeat/sdk-python (mirrored from
4
+ # eu-security-app sdk/python). It does NOT run in the monorepo — GitHub only
5
+ # executes workflows at the repository root.
6
+ #
7
+ # Auth: PyPI Trusted Publishing (OIDC) — no API token stored. Configure the
8
+ # trusted publisher once on PyPI: project "trustbeat" → Publishing → add
9
+ # GitHub publisher (owner: TrustBeat, repo: sdk-python, workflow: publish.yml).
10
+ name: Publish to PyPI
11
+
12
+ on:
13
+ push:
14
+ tags: ['v*']
15
+ workflow_dispatch:
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ publish:
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ id-token: write # required for Trusted Publishing
25
+ contents: read
26
+ steps:
27
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
28
+
29
+ - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
30
+ with:
31
+ python-version: '3.12'
32
+
33
+ - name: Verify tag matches package version
34
+ if: startsWith(github.ref, 'refs/tags/')
35
+ run: |
36
+ tag="${GITHUB_REF_NAME#v}"
37
+ pkg=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
38
+ echo "tag=$tag pkg=$pkg"
39
+ [ "$tag" = "$pkg" ] || { echo "::error::tag $tag != pyproject version $pkg"; exit 1; }
40
+
41
+ - name: Run tests
42
+ run: python -m unittest discover -s tests -v
43
+
44
+ - name: Build sdist + wheel
45
+ run: |
46
+ python -m pip install --upgrade build
47
+ python -m build
48
+
49
+ - name: Publish to PyPI
50
+ uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ .coverage
9
+ htmlcov/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Trustbeat s.r.o.
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,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: trustbeat
3
+ Version: 0.1.0
4
+ Summary: TrustBeat SDK — qualified timestamping and Merkle anchoring
5
+ Project-URL: Homepage, https://trustbeat.eu
6
+ Project-URL: Repository, https://github.com/TrustBeat/sdk-python
7
+ Project-URL: Issues, https://github.com/TrustBeat/sdk-python/issues
8
+ Author-email: "Trustbeat s.r.o." <radim.dejmek@trustbeat.eu>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: anchoring,eidas,merkle,qualified,rfc3161,timestamp,trust
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Security :: Cryptography
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Requires-Dist: pytest>=7; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # TrustBeat Python SDK
30
+
31
+ Qualified electronic timestamps and Merkle anchoring — eIDAS-compliant, over a simple API.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install trustbeat
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ ```python
42
+ from trustbeat import TrustBeat
43
+
44
+ tb = TrustBeat(api_key="tb_live_...")
45
+
46
+ # Anchor a file (SHA-256 computed locally, file never leaves your machine).
47
+ # anchor_file_wait() blocks until the proof is ready (next batch, up to 11 min).
48
+ proof = tb.anchor_file_wait("contract.pdf")
49
+ print(proof.id) # tracking ID
50
+ print(proof.anchored_at) # ISO 8601 timestamp
51
+ print(proof.merkle_root) # Merkle root of the batch
52
+
53
+ # Verify locally — no network call
54
+ assert tb.verify(proof)
55
+
56
+ # Or anchor a raw SHA-256 hash without blocking, then wait for the proof.
57
+ job = tb.anchor("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
58
+ print(job.id) # tracking ID, returned immediately (202)
59
+ proof = tb.anchor_wait(job.id) # blocks up to 11 min
60
+
61
+ ```
62
+
63
+ ## Requirements
64
+
65
+ - Python 3.9+
66
+ - Zero runtime dependencies (stdlib only)
67
+
68
+ ## Documentation
69
+
70
+ Full API reference and guides at [api.trustbeat.eu/docs](https://api.trustbeat.eu/docs)
71
+
72
+ ## License
73
+
74
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,46 @@
1
+ # TrustBeat Python SDK
2
+
3
+ Qualified electronic timestamps and Merkle anchoring — eIDAS-compliant, over a simple API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install trustbeat
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ from trustbeat import TrustBeat
15
+
16
+ tb = TrustBeat(api_key="tb_live_...")
17
+
18
+ # Anchor a file (SHA-256 computed locally, file never leaves your machine).
19
+ # anchor_file_wait() blocks until the proof is ready (next batch, up to 11 min).
20
+ proof = tb.anchor_file_wait("contract.pdf")
21
+ print(proof.id) # tracking ID
22
+ print(proof.anchored_at) # ISO 8601 timestamp
23
+ print(proof.merkle_root) # Merkle root of the batch
24
+
25
+ # Verify locally — no network call
26
+ assert tb.verify(proof)
27
+
28
+ # Or anchor a raw SHA-256 hash without blocking, then wait for the proof.
29
+ job = tb.anchor("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
30
+ print(job.id) # tracking ID, returned immediately (202)
31
+ proof = tb.anchor_wait(job.id) # blocks up to 11 min
32
+
33
+ ```
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.9+
38
+ - Zero runtime dependencies (stdlib only)
39
+
40
+ ## Documentation
41
+
42
+ Full API reference and guides at [api.trustbeat.eu/docs](https://api.trustbeat.eu/docs)
43
+
44
+ ## License
45
+
46
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "trustbeat"
7
+ version = "0.1.0"
8
+ description = "TrustBeat SDK — qualified timestamping and Merkle anchoring"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Trustbeat s.r.o.", email = "radim.dejmek@trustbeat.eu" }]
13
+ keywords = ["eidas", "timestamp", "rfc3161", "merkle", "anchoring", "trust", "qualified"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Security :: Cryptography",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ ]
27
+ dependencies = [] # zero runtime dependencies — stdlib only
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=7", # optional — tests also run with stdlib unittest
32
+ "pytest-cov",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://trustbeat.eu"
37
+ Repository = "https://github.com/TrustBeat/sdk-python"
38
+ Issues = "https://github.com/TrustBeat/sdk-python/issues"
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+
43
+ [tool.unittest]
44
+ start-dir = "tests"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["trustbeat"]
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TrustBeat Python SDK smoke CLI — drives the SDK against a LIVE API.
4
+
5
+ Driven by tests/e2e/sdk_smoke.py (the orchestrator), not run directly in CI.
6
+ Two commands:
7
+
8
+ submit anchor TB_HASH, print the tracking id on stdout
9
+ verify <id> fetch the proof via the SDK, check the contract, verify locally
10
+
11
+ Env:
12
+ TB_BASE_URL API base URL (e.g. http://localhost:8080)
13
+ TB_API_KEY provisioned account API key
14
+ TB_HASH SHA-256 hex the SDK anchors / echoes back
15
+
16
+ Exit 0 on success, non-zero on any failure.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import os
23
+ import sys
24
+
25
+ # smoke.py lives next to the `trustbeat` package, so a plain import works when
26
+ # this file's directory is on sys.path[0] (the default for `python smoke.py`).
27
+ from trustbeat import TrustBeat, AiDecisionMetadata, AiTimeEnvelope
28
+
29
+
30
+ # Fixed AI-decision metadata — only the input/output hashes vary per run.
31
+ _AI_META = AiDecisionMetadata(
32
+ model_id="claude-opus-4-8",
33
+ system_name="trustbeat-sdk-smoke",
34
+ risk_category="employment",
35
+ decision_type="classification",
36
+ human_oversight=True,
37
+ time_envelope=AiTimeEnvelope(
38
+ started_at="2026-06-29T10:00:00Z",
39
+ completed_at="2026-06-29T10:00:01Z",
40
+ ),
41
+ )
42
+
43
+
44
+ def _client() -> TrustBeat:
45
+ base = os.environ["TB_BASE_URL"]
46
+ key = os.environ["TB_API_KEY"]
47
+ return TrustBeat(key, base_url=base)
48
+
49
+
50
+ def cmd_submit() -> int:
51
+ h = os.environ["TB_HASH"]
52
+ job = _client().anchor(h)
53
+ if not job.id:
54
+ print("submit: empty tracking id", file=sys.stderr)
55
+ return 1
56
+ print(job.id)
57
+ return 0
58
+
59
+
60
+ def cmd_verify(tracking_id: str) -> int:
61
+ expected = os.environ.get("TB_HASH")
62
+ tb = _client()
63
+
64
+ proof = tb.get_proof(tracking_id)
65
+ if proof is None:
66
+ print(f"verify: proof for {tracking_id} not ready", file=sys.stderr)
67
+ return 1
68
+
69
+ # Contract checks — these are what mocks never exercise.
70
+ if expected and proof.hash.lower() != expected.lower():
71
+ print(f"verify: hash echo mismatch {proof.hash} != {expected}", file=sys.stderr)
72
+ return 1
73
+ if not proof.merkle_root:
74
+ print("verify: empty merkle_root", file=sys.stderr)
75
+ return 1
76
+ if not proof.token:
77
+ print("verify: empty token", file=sys.stderr)
78
+ return 1
79
+
80
+ # Offline Merkle verification through the SDK.
81
+ if tb.verify(proof) is not True:
82
+ print("verify: local Merkle verification failed", file=sys.stderr)
83
+ return 1
84
+
85
+ print(f"OK id={tracking_id} root={proof.merkle_root[:16]}… token={len(proof.token)}B")
86
+ return 0
87
+
88
+
89
+ def _batch_hashes() -> list[str]:
90
+ seed = os.environ["TB_BATCH_SEED"]
91
+ n = int(os.environ["TB_BATCH_N"])
92
+ return [hashlib.sha256(f"{seed}::{i}".encode()).hexdigest() for i in range(n)]
93
+
94
+
95
+ def cmd_submit_batch() -> int:
96
+ hashes = _batch_hashes()
97
+ sub = _client().anchor_batch(hashes)
98
+ if not sub.submission_id:
99
+ print("submit-batch: empty submission_id", file=sys.stderr)
100
+ return 1
101
+ if len(sub.items) != len(hashes):
102
+ print(f"submit-batch: accepted {len(sub.items)} != {len(hashes)}", file=sys.stderr)
103
+ return 1
104
+ print(sub.submission_id)
105
+ return 0
106
+
107
+
108
+ def cmd_verify_batch(submission_id: str) -> int:
109
+ expected = {h.lower() for h in _batch_hashes()}
110
+ tb = _client()
111
+
112
+ proofs = tb.get_batch_proofs(submission_id)
113
+ if len(proofs) != len(expected):
114
+ print(f"verify-batch: got {len(proofs)} proofs, want {len(expected)}", file=sys.stderr)
115
+ return 1
116
+ if {p.hash.lower() for p in proofs} != expected:
117
+ print("verify-batch: proof hashes do not match submitted set", file=sys.stderr)
118
+ return 1
119
+ for p in proofs:
120
+ if not p.merkle_root or not p.token:
121
+ print(f"verify-batch: empty merkle_root/token for {p.id}", file=sys.stderr)
122
+ return 1
123
+ if tb.verify(p) is not True:
124
+ print(f"verify-batch: local Merkle verification failed for {p.id}", file=sys.stderr)
125
+ return 1
126
+
127
+ print(f"OK batch sid={submission_id} n={len(proofs)}")
128
+ return 0
129
+
130
+
131
+ # ── AI decision (EU AI Act Art. 12) ───────────────────────────────────────────
132
+
133
+ def cmd_submit_ai() -> int:
134
+ in_hash = os.environ["TB_AI_INPUT"]
135
+ out_hash = os.environ["TB_AI_OUTPUT"]
136
+ job = _client().anchor_ai_decision(in_hash, out_hash, _AI_META)
137
+ if not job.id:
138
+ print("submit-ai: empty tracking id", file=sys.stderr)
139
+ return 1
140
+ print(job.id)
141
+ return 0
142
+
143
+
144
+ def cmd_verify_ai(tracking_id: str) -> int:
145
+ in_hash = os.environ["TB_AI_INPUT"]
146
+ out_hash = os.environ["TB_AI_OUTPUT"]
147
+ tb = _client()
148
+
149
+ proof = tb.get_ai_decision_proof(tracking_id)
150
+ if proof is None:
151
+ print(f"verify-ai: proof for {tracking_id} not ready", file=sys.stderr)
152
+ return 1
153
+ if proof.input_hash.lower() != in_hash.lower():
154
+ print(f"verify-ai: input_hash echo mismatch {proof.input_hash} != {in_hash}", file=sys.stderr)
155
+ return 1
156
+ if proof.output_hash.lower() != out_hash.lower():
157
+ print(f"verify-ai: output_hash echo mismatch {proof.output_hash} != {out_hash}", file=sys.stderr)
158
+ return 1
159
+ if proof.verification_status != "VERIFIED":
160
+ print(f"verify-ai: status {proof.verification_status} != VERIFIED", file=sys.stderr)
161
+ return 1
162
+ if proof.proof is None:
163
+ print("verify-ai: missing Merkle proof", file=sys.stderr)
164
+ return 1
165
+ if tb.verify(proof.proof) is not True:
166
+ print("verify-ai: local Merkle verification failed", file=sys.stderr)
167
+ return 1
168
+
169
+ print(f"OK ai id={tracking_id} combined={proof.combined_hash[:16]}…")
170
+ return 0
171
+
172
+
173
+ # ── File anchoring ────────────────────────────────────────────────────────────
174
+ # verify reuses `verify` with TB_HASH set to the file's SHA-256.
175
+
176
+ def cmd_submit_file() -> int:
177
+ path = os.environ["TB_FILE_PATH"]
178
+ job = _client().anchor_file(path)
179
+ if not job.id:
180
+ print("submit-file: empty tracking id", file=sys.stderr)
181
+ return 1
182
+ print(job.id)
183
+ return 0
184
+
185
+
186
+ # ── NIS2 audit trail ──────────────────────────────────────────────────────────
187
+
188
+ def cmd_submit_audit() -> int:
189
+ event_id = _client().submit_audit_event(
190
+ os.environ["TB_AUDIT_CATEGORY"],
191
+ os.environ["TB_AUDIT_ACTOR"],
192
+ os.environ["TB_AUDIT_ACTION"],
193
+ os.environ["TB_AUDIT_TS"],
194
+ )
195
+ if not event_id:
196
+ print("submit-audit: empty event_id", file=sys.stderr)
197
+ return 1
198
+ print(event_id)
199
+ return 0
200
+
201
+
202
+ def cmd_verify_audit(event_id: str) -> int:
203
+ tb = _client()
204
+ proof = tb.get_audit_event_proof(event_id)
205
+ if proof is None:
206
+ print(f"verify-audit: proof for {event_id} not ready", file=sys.stderr)
207
+ return 1
208
+ if proof.event_id != event_id:
209
+ print(f"verify-audit: event_id echo mismatch {proof.event_id} != {event_id}", file=sys.stderr)
210
+ return 1
211
+ if not proof.canonical_hash:
212
+ print("verify-audit: empty canonical_hash", file=sys.stderr)
213
+ return 1
214
+ if not proof.batch_id:
215
+ print("verify-audit: empty batch_id", file=sys.stderr)
216
+ return 1
217
+ if proof.leaf_index < 0 or proof.merkle_path is None:
218
+ print("verify-audit: invalid leaf_index/merkle_path", file=sys.stderr)
219
+ return 1
220
+
221
+ # The event must also surface through the query endpoint.
222
+ events = tb.list_audit_events(trail_category=os.environ["TB_AUDIT_CATEGORY"])
223
+ if not any(e.event_id == event_id for e in events):
224
+ print(f"verify-audit: {event_id} not returned by list_audit_events", file=sys.stderr)
225
+ return 1
226
+
227
+ print(f"OK audit id={event_id} batch={proof.batch_id[:12]}… leaf={proof.leaf_index}")
228
+ return 0
229
+
230
+
231
+ # ── Signature & certificate verification (synchronous) ────────────────────────
232
+
233
+ def cmd_verify_sig() -> int:
234
+ with open(os.environ["TB_SIG_DOC"], "rb") as f:
235
+ doc = f.read()
236
+ expected = os.environ["TB_SIG_DOCHASH"]
237
+ report = _client().verify_signature(doc, os.environ["TB_SIG_FORMAT"])
238
+ if report.document_hash.lower() != expected.lower():
239
+ print(f"verify-sig: document_hash mismatch {report.document_hash} != {expected}", file=sys.stderr)
240
+ return 1
241
+ if not report.verdict:
242
+ print("verify-sig: empty verdict", file=sys.stderr)
243
+ return 1
244
+ if not report.signatures:
245
+ print("verify-sig: report has no signatures", file=sys.stderr)
246
+ return 1
247
+ print(f"OK sig verdict={report.verdict} signatures={len(report.signatures)}")
248
+ return 0
249
+
250
+
251
+ def cmd_validate_cert() -> int:
252
+ with open(os.environ["TB_CERT_PATH"], "rb") as f:
253
+ cert = f.read()
254
+ res = _client().validate_certificate(cert)
255
+ if not res.subject:
256
+ print("validate-cert: empty subject", file=sys.stderr)
257
+ return 1
258
+ if not res.issuer:
259
+ print("validate-cert: empty issuer", file=sys.stderr)
260
+ return 1
261
+ if not res.validated_at:
262
+ print("validate-cert: empty validated_at", file=sys.stderr)
263
+ return 1
264
+ print(f"OK cert subject={res.subject[:24]}… qualified={res.qualified}")
265
+ return 0
266
+
267
+
268
+ def main() -> int:
269
+ if len(sys.argv) < 2:
270
+ print("usage: smoke.py {submit|verify <id>|submit-batch|verify-batch <id>|"
271
+ "submit-ai|verify-ai <id>|submit-file|submit-audit|verify-audit <id>|"
272
+ "verify-sig|validate-cert}", file=sys.stderr)
273
+ return 2
274
+ cmd = sys.argv[1]
275
+ if cmd == "submit":
276
+ return cmd_submit()
277
+ if cmd == "verify":
278
+ if len(sys.argv) < 3:
279
+ print("usage: smoke.py verify <id>", file=sys.stderr)
280
+ return 2
281
+ return cmd_verify(sys.argv[2])
282
+ if cmd == "submit-batch":
283
+ return cmd_submit_batch()
284
+ if cmd == "verify-batch":
285
+ if len(sys.argv) < 3:
286
+ print("usage: smoke.py verify-batch <id>", file=sys.stderr)
287
+ return 2
288
+ return cmd_verify_batch(sys.argv[2])
289
+ if cmd == "submit-ai":
290
+ return cmd_submit_ai()
291
+ if cmd == "verify-ai":
292
+ if len(sys.argv) < 3:
293
+ print("usage: smoke.py verify-ai <id>", file=sys.stderr)
294
+ return 2
295
+ return cmd_verify_ai(sys.argv[2])
296
+ if cmd == "submit-file":
297
+ return cmd_submit_file()
298
+ if cmd == "submit-audit":
299
+ return cmd_submit_audit()
300
+ if cmd == "verify-audit":
301
+ if len(sys.argv) < 3:
302
+ print("usage: smoke.py verify-audit <id>", file=sys.stderr)
303
+ return 2
304
+ return cmd_verify_audit(sys.argv[2])
305
+ if cmd == "verify-sig":
306
+ return cmd_verify_sig()
307
+ if cmd == "validate-cert":
308
+ return cmd_validate_cert()
309
+ print(f"unknown command: {cmd}", file=sys.stderr)
310
+ return 2
311
+
312
+
313
+ if __name__ == "__main__":
314
+ sys.exit(main())
File without changes