vbase-api 0.1.2__tar.gz → 0.1.4__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,17 @@
1
+ # Changelog
2
+
3
+ All notable changes are documented here, following
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ Earlier releases were not tracked in this file; see the
8
+ [commit history](https://github.com/validityBase/vbase-api-py/commits/main/)
9
+ for prior changes.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [0.1.4] - 2026-10-26
14
+
15
+ ### Added
16
+
17
+ - Compatibility test matrix for Python 3.8, 3.11, and 3.12.
@@ -0,0 +1,2 @@
1
+ include requirements.in
2
+ include CHANGELOG.md
@@ -1,25 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vbase-api
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: vBase REST API Python Client
5
5
  Author-email: vBase <tech@vbase.com>
6
- License-Expression: Apache-2.0
6
+ License: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/validityBase/vbase-api-py
8
8
  Project-URL: Documentation, https://docs.vbase.com/
9
9
  Keywords: vbase,stamping,web3,blockchain,validation
10
10
  Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
11
14
  Classifier: Operating System :: OS Independent
12
15
  Requires-Python: >=3.8
13
16
  Description-Content-Type: text/markdown
14
17
  License-File: LICENSE.txt
15
18
  License-File: NOTICE.txt
16
- Requires-Dist: requests>=2.7.0
19
+ Requires-Dist: requests<3,>=2.7.0
17
20
  Dynamic: license-file
18
21
 
19
- # vbase-py
22
+ # vbase-api-py
20
23
 
21
24
  vBase REST API Python Client
22
25
 
26
+ Requires Python 3.11 or newer.
27
+
23
28
  See [documentation](https://docs.vbase.com/) and the [Swagger UI](https://app.vbase.com/swagger/) for more details.
24
29
 
25
30
  ---
@@ -1,7 +1,9 @@
1
- # vbase-py
1
+ # vbase-api-py
2
2
 
3
3
  vBase REST API Python Client
4
4
 
5
+ Requires Python 3.11 or newer.
6
+
5
7
  See [documentation](https://docs.vbase.com/) and the [Swagger UI](https://app.vbase.com/swagger/) for more details.
6
8
 
7
9
  ---
@@ -1,38 +1,38 @@
1
- [build-system]
2
- requires = ["setuptools >= 77.0.3"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "vbase-api" # This is the name on PyPI
7
- dynamic = ["version"]
8
- description = "vBase REST API Python Client"
9
- readme = "README.md"
10
- requires-python = ">=3.8"
11
- dependencies = [
12
- "requests>=2.7.0"
13
- ]
14
- license = "Apache-2.0"
15
- authors = [
16
- { name = "vBase", email = "tech@vbase.com" }
17
- ]
18
-
19
- keywords = ["vbase", "stamping", "web3", "blockchain", "validation"]
20
- classifiers = [
21
- "Programming Language :: Python :: 3",
22
- "Operating System :: OS Independent",
23
- ]
24
-
25
- [project.urls]
26
- Homepage = "https://github.com/validityBase/vbase-api-py"
27
- Documentation = "https://docs.vbase.com/"
28
-
29
- # This tells setuptools where to find the code
30
- [tool.setuptools]
31
- package-dir = {"" = "src"}
32
-
33
- [tool.setuptools.packages.find]
34
- where = ["src"]
35
-
36
- [tool.setuptools.dynamic]
37
- version = {attr = "vbase_api._version.__version__"}
38
-
1
+ [build-system]
2
+ requires = ["setuptools >= 62.6"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vbase-api" # This is the name on PyPI
7
+ dynamic = ["version", "dependencies"]
8
+ description = "vBase REST API Python Client"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "Apache-2.0"}
12
+ authors = [
13
+ { name = "vBase", email = "tech@vbase.com" }
14
+ ]
15
+
16
+ keywords = ["vbase", "stamping", "web3", "blockchain", "validation"]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Operating System :: OS Independent",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/validityBase/vbase-api-py"
27
+ Documentation = "https://docs.vbase.com/"
28
+
29
+ # This tells setuptools where to find the code
30
+ [tool.setuptools]
31
+ package-dir = {"" = "src"}
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.setuptools.dynamic]
37
+ version = {attr = "vbase_api._version.__version__"}
38
+ dependencies = {file = ["requirements.in"]}
@@ -0,0 +1 @@
1
+ requests>=2.7.0,<3
@@ -0,0 +1 @@
1
+ __version__ = "0.1.4"
@@ -49,9 +49,11 @@ class VBaseAPIClient:
49
49
  timeout: Request timeout in seconds (default: 30)
50
50
 
51
51
  Example:
52
- >>> client = VBaseAPIClient(api_key="your-bearer-token")
53
- >>> collections = client.list_collections()
54
- >>> stamp = client.create_stamp(data={"hello": "world"})
52
+ .. code-block:: python
53
+
54
+ client = VBaseAPIClient(api_key="your-bearer-token")
55
+ collections = client.list_collections()
56
+ stamp = client.create_stamp(data={"hello": "world"})
55
57
  """
56
58
 
57
59
  DEFAULT_BASE_URL = "https://app.vbase.com"
@@ -146,9 +148,11 @@ class VBaseAPIClient:
146
148
  VBaseAPIError: If the request fails
147
149
 
148
150
  Example:
149
- >>> collections = client.get_collections(is_pinned=True)
150
- >>> for collection in collections:
151
- ... print(f"{collection.name}: {collection.cid}")
151
+ .. code-block:: python
152
+
153
+ collections = client.get_collections(is_pinned=True)
154
+ for collection in collections:
155
+ print(f"{collection.name}: {collection.cid}")
152
156
  """
153
157
  params = {}
154
158
  if user_address is not None:
@@ -187,13 +191,15 @@ class VBaseAPIClient:
187
191
  VBaseAPIError: If the request fails or collection already exists
188
192
 
189
193
  Example:
190
- >>> collection = client.create_collection(
191
- ... name="My Collection",
192
- ... cid="0x1234567890abcdef...",
193
- ... description="A sample collection",
194
- ... is_pinned=True
195
- ... )
196
- >>> print(f"Created: {collection.name}")
194
+ .. code-block:: python
195
+
196
+ collection = client.create_collection(
197
+ name="My Collection",
198
+ cid="0x1234567890abcdef...",
199
+ description="A sample collection",
200
+ is_pinned=True
201
+ )
202
+ print(f"Created: {collection.name}")
197
203
  """
198
204
  data = {
199
205
  'name': name,
@@ -252,17 +258,19 @@ class VBaseAPIClient:
252
258
  ValueError: If invalid parameters are provided
253
259
 
254
260
  Example:
255
- >>> # Stamp inline data
256
- >>> stamp = client.create_stamp(data={"hello": "world"})
257
- >>> print(f"Object CID: {stamp.commitment_receipt.object_cid}")
258
-
259
- >>> # Stamp a file
260
- >>> stamp = client.create_stamp(file="document.pdf", collection_name="Documents")
261
- >>> if stamp.file_object:
262
- ... print(f"File: {stamp.file_object.file_name}")
263
-
264
- >>> # Stamp an existing CID
265
- >>> stamp = client.create_stamp(data_cid="Qm...")
261
+ .. code-block:: python
262
+
263
+ # Stamp inline data
264
+ stamp = client.create_stamp(data={"hello": "world"})
265
+ print(f"Object CID: {stamp.commitment_receipt.object_cid}")
266
+
267
+ # Stamp a file
268
+ stamp = client.create_stamp(file="document.pdf", collection_name="Documents")
269
+ if stamp.file_object:
270
+ print(f"File: {stamp.file_object.file_name}")
271
+
272
+ # Stamp an existing CID
273
+ stamp = client.create_stamp(data_cid="Qm...")
266
274
  """
267
275
  if not any([file, data, data_cid]):
268
276
  raise ValueError("At least one of 'file', 'data', or 'data_cid' must be provided")
@@ -337,11 +345,13 @@ class VBaseAPIClient:
337
345
  VBaseAPIError: If validation fails or file not found in blockchain
338
346
 
339
347
  Example:
340
- >>> result = client.upload_stamped_file(
341
- ... collection_name="My Collection",
342
- ... file="stamped_document.pdf"
343
- ... )
344
- >>> print(f"Uploaded: {result.file_object.file_name}")
348
+ .. code-block:: python
349
+
350
+ result = client.upload_stamped_file(
351
+ collection_name="My Collection",
352
+ file="stamped_document.pdf"
353
+ )
354
+ print(f"Uploaded: {result.file_object.file_name}")
345
355
  """
346
356
  form_data = {
347
357
  'collection_name': collection_name
@@ -386,12 +396,14 @@ class VBaseAPIClient:
386
396
  VBaseAPIError: If the request fails
387
397
 
388
398
  Example:
389
- >>> result = client.verify_stamps(
390
- ... cids=["0xbd...1", "0xcd...2"],
391
- ... filter_by_user=True
392
- ... )
393
- >>> for stamp in result.stamp_list:
394
- ... print(f"Found stamp at {stamp.timestamp}")
399
+ .. code-block:: python
400
+
401
+ result = client.verify_stamps(
402
+ cids=["0xbd...1", "0xcd...2"],
403
+ filter_by_user=True
404
+ )
405
+ for stamp in result.stamp_list:
406
+ print(f"Found stamp at {stamp.timestamp}")
395
407
  """
396
408
  data = {
397
409
  'cids': cids,
@@ -421,8 +433,10 @@ class VBaseAPIClient:
421
433
  VBaseAPIError: If the request fails
422
434
 
423
435
  Example:
424
- >>> user = client.get_current_user()
425
- >>> print(f"User email: {user.email}")
436
+ .. code-block:: python
437
+
438
+ user = client.get_current_user()
439
+ print(f"User email: {user.email}")
426
440
  """
427
441
  response = self.session.get(
428
442
  self._get_url('users/me'),
@@ -445,8 +459,10 @@ class VBaseAPIClient:
445
459
  VBaseAPIError: If the request fails or user not found
446
460
 
447
461
  Example:
448
- >>> user = client.get_user("0x...")
449
- >>> print(f"User name: {user.name}")
462
+ .. code-block:: python
463
+
464
+ user = client.get_user("0x...")
465
+ print(f"User name: {user.name}")
450
466
  """
451
467
  response = self.session.get(
452
468
  self._get_url(f'users/{user_address}'),
@@ -486,7 +502,9 @@ def create_client(
486
502
  Configured VBaseAPIClient instance
487
503
 
488
504
  Example:
489
- >>> client = create_client(api_key="your-bearer-token")
490
- >>> collections = client.get_collections()
505
+ .. code-block:: python
506
+
507
+ client = create_client(api_key="your-bearer-token")
508
+ collections = client.get_collections()
491
509
  """
492
510
  return VBaseAPIClient(api_key=api_key, base_url=base_url, timeout=timeout)
@@ -1,25 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vbase-api
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: vBase REST API Python Client
5
5
  Author-email: vBase <tech@vbase.com>
6
- License-Expression: Apache-2.0
6
+ License: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/validityBase/vbase-api-py
8
8
  Project-URL: Documentation, https://docs.vbase.com/
9
9
  Keywords: vbase,stamping,web3,blockchain,validation
10
10
  Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
11
14
  Classifier: Operating System :: OS Independent
12
15
  Requires-Python: >=3.8
13
16
  Description-Content-Type: text/markdown
14
17
  License-File: LICENSE.txt
15
18
  License-File: NOTICE.txt
16
- Requires-Dist: requests>=2.7.0
19
+ Requires-Dist: requests<3,>=2.7.0
17
20
  Dynamic: license-file
18
21
 
19
- # vbase-py
22
+ # vbase-api-py
20
23
 
21
24
  vBase REST API Python Client
22
25
 
26
+ Requires Python 3.11 or newer.
27
+
23
28
  See [documentation](https://docs.vbase.com/) and the [Swagger UI](https://app.vbase.com/swagger/) for more details.
24
29
 
25
30
  ---
@@ -1,7 +1,10 @@
1
+ CHANGELOG.md
1
2
  LICENSE.txt
3
+ MANIFEST.in
2
4
  NOTICE.txt
3
5
  README.md
4
6
  pyproject.toml
7
+ requirements.in
5
8
  src/vbase_api/__init__.py
6
9
  src/vbase_api/_version.py
7
10
  src/vbase_api/vbase_api_client.py
@@ -10,4 +13,6 @@ src/vbase_api.egg-info/PKG-INFO
10
13
  src/vbase_api.egg-info/SOURCES.txt
11
14
  src/vbase_api.egg-info/dependency_links.txt
12
15
  src/vbase_api.egg-info/requires.txt
13
- src/vbase_api.egg-info/top_level.txt
16
+ src/vbase_api.egg-info/top_level.txt
17
+ tests/test_api_client.py
18
+ tests/test_file_integrity_e2e.py
@@ -0,0 +1 @@
1
+ requests<3,>=2.7.0
@@ -0,0 +1,107 @@
1
+ """Client request/response contracts that run without live services."""
2
+
3
+ import json
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ import requests
10
+
11
+ import vbase_api
12
+
13
+
14
+ class APIClientTests(unittest.TestCase):
15
+ def setUp(self):
16
+ self.client = vbase_api.VBaseAPIClient(
17
+ api_key="test-only-key", base_url="https://example.invalid", timeout=7
18
+ )
19
+ self.addCleanup(self.client.close)
20
+ self.receipt = {
21
+ "transaction_hash": "0xtransaction",
22
+ "user_address": "0xuser",
23
+ "set_cid": "0xset",
24
+ "object_cid": "0xobject",
25
+ "timestamp": "2026-10-26T00:00:00Z",
26
+ "chain_id": 1,
27
+ }
28
+
29
+ @staticmethod
30
+ def response(status, payload):
31
+ response = requests.Response()
32
+ response.status_code = status
33
+ response._content = json.dumps(payload).encode("utf-8")
34
+ return response
35
+
36
+ def test_collection_request_and_response(self):
37
+ payload = {
38
+ "id": 1,
39
+ "name": "Example",
40
+ "cid": "0xset",
41
+ "is_pinned": True,
42
+ "is_public": False,
43
+ "created_at": "2026-10-26T00:00:00Z",
44
+ "description": "Example collection",
45
+ }
46
+ with patch.object(
47
+ self.client.session, "post", return_value=self.response(201, payload)
48
+ ) as post:
49
+ collection = self.client.create_collection("Example", "Example collection")
50
+ self.assertEqual(collection.cid, "0xset")
51
+ self.assertEqual(collection.name, "Example")
52
+ post.assert_called_once_with(
53
+ "https://example.invalid/api/v1/collections",
54
+ json={
55
+ "name": "Example",
56
+ "cid": None,
57
+ "description": "Example collection",
58
+ "is_pinned": True,
59
+ },
60
+ timeout=7,
61
+ )
62
+
63
+ def test_stamp_response_types(self):
64
+ for status, expected in [
65
+ (200, vbase_api.IdempotentStampResponse),
66
+ (201, vbase_api.StampCreatedResponse),
67
+ ]:
68
+ with self.subTest(status=status):
69
+ with patch.object(
70
+ self.client.session,
71
+ "post",
72
+ return_value=self.response(
73
+ status, {"commitment_receipt": self.receipt}
74
+ ),
75
+ ):
76
+ stamp = self.client.create_stamp(data_cid="0xobject")
77
+ self.assertIsInstance(stamp, expected)
78
+ self.assertEqual(stamp.commitment_receipt.object_cid, "0xobject")
79
+
80
+ def test_upload_closes_file_after_request(self):
81
+ with tempfile.TemporaryDirectory() as directory:
82
+ path = Path(directory) / "example.bin"
83
+ path.write_bytes(b"example")
84
+ with patch.object(
85
+ self.client.session,
86
+ "post",
87
+ return_value=self.response(201, {"commitment_receipt": self.receipt}),
88
+ ) as post:
89
+ result = self.client.upload_stamped_file("Example", path)
90
+ uploaded = post.call_args.kwargs["files"]["file"][1]
91
+ self.assertTrue(uploaded.closed)
92
+ self.assertEqual(result.commitment_receipt.object_cid, "0xobject")
93
+
94
+ def test_api_error_preserves_status_and_message(self):
95
+ with patch.object(
96
+ self.client.session,
97
+ "get",
98
+ return_value=self.response(403, {"error": "Access denied"}),
99
+ ):
100
+ with self.assertRaises(vbase_api.VBaseAPIError) as raised:
101
+ self.client.get_collections()
102
+ self.assertEqual(raised.exception.status_code, 403)
103
+ self.assertEqual(str(raised.exception), "[403] Access denied")
104
+
105
+
106
+ if __name__ == "__main__":
107
+ unittest.main()
@@ -0,0 +1,400 @@
1
+ """Live file-integrity E2E coverage for the vBase API Python client.
2
+
3
+ These tests call the public ``VBaseAPIClient`` stamping methods and then read
4
+ the stored object from S3 to verify the final bytes. They are skipped unless
5
+ live E2E credentials are supplied through GitHub Actions secrets or equivalent
6
+ environment variables.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import importlib.metadata as metadata
13
+ import os
14
+ import tempfile
15
+ import time
16
+ import unittest
17
+ import uuid
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Iterable, Optional
21
+
22
+ try:
23
+ import boto3
24
+ from botocore.exceptions import BotoCoreError, ClientError
25
+ except ImportError as exc: # pragma: no cover
26
+ raise unittest.SkipTest(
27
+ "Install requirements/e2e.txt to run live file-integrity E2E tests."
28
+ ) from exc
29
+
30
+ import vbase_api
31
+ from vbase_api import VBaseAPIClient, VBaseAPIError
32
+
33
+
34
+ def _sha3_256_cid(payload: bytes) -> str:
35
+ return "0x" + hashlib.sha3_256(payload).hexdigest()
36
+
37
+
38
+ def _first_env(*names: str) -> Optional[str]:
39
+ for name in names:
40
+ value = os.environ.get(name)
41
+ if value:
42
+ return value
43
+ return None
44
+
45
+
46
+ def _int_env(name: str, default: int) -> int:
47
+ value = _first_env(name)
48
+ if value is None:
49
+ return default
50
+
51
+ try:
52
+ return int(value)
53
+ except ValueError:
54
+ raise ValueError(
55
+ f"{name} must be an integer number of seconds; got {value!r}."
56
+ ) from None
57
+
58
+
59
+ def _log_and_assert_package_source() -> None:
60
+ install_source = _first_env("VBASE_API_PACKAGE_SOURCE")
61
+ if not install_source:
62
+ return
63
+
64
+ package_path = Path(vbase_api.__file__).resolve()
65
+ package_version = getattr(vbase_api, "__version__", "unknown")
66
+ try:
67
+ distribution_version = metadata.version("vbase-api")
68
+ except metadata.PackageNotFoundError:
69
+ distribution_version = "not installed"
70
+ print(
71
+ "vbase-api "
72
+ f"package_version={package_version} "
73
+ f"distribution_version={distribution_version} "
74
+ f"imported from {package_path}"
75
+ )
76
+
77
+ repo_root_raw = _first_env("GITHUB_WORKSPACE")
78
+ if not repo_root_raw:
79
+ return
80
+
81
+ repo_root = Path(repo_root_raw).resolve()
82
+ try:
83
+ package_path.relative_to(repo_root)
84
+ package_in_repo = True
85
+ except ValueError:
86
+ package_in_repo = False
87
+
88
+ if install_source == "source":
89
+ if not package_in_repo:
90
+ raise AssertionError(
91
+ "Source-install E2E run imported vbase_api outside the checkout: "
92
+ f"{package_path}"
93
+ )
94
+ elif install_source == "pypi" and package_in_repo:
95
+ raise AssertionError(
96
+ "PyPI-install E2E run imported vbase_api from the checkout instead of "
97
+ f"site-packages: {package_path}"
98
+ )
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class LiveE2EConfig:
103
+ """Configuration needed for live app/API file-integrity tests."""
104
+
105
+ api_key: str
106
+ base_url: str
107
+ s3_bucket: str
108
+ aws_access_key_id: str
109
+ aws_secret_access_key: str
110
+ aws_session_token: Optional[str]
111
+ aws_region: str
112
+ timeout: int
113
+
114
+ @classmethod
115
+ def from_env(cls) -> "LiveE2EConfig":
116
+ api_key = _first_env("VBASE_API_KEY")
117
+ if not api_key:
118
+ raise unittest.SkipTest(
119
+ "Set VBASE_API_KEY to run live file-integrity E2E tests."
120
+ )
121
+
122
+ s3_bucket = _first_env("S3_VALIDATION_BUCKET")
123
+ if not s3_bucket:
124
+ raise unittest.SkipTest(
125
+ "Set S3_VALIDATION_BUCKET to run live file-integrity E2E tests."
126
+ )
127
+
128
+ aws_access_key_id = _first_env("AWS_ACCESS_KEY_ID")
129
+ aws_secret_access_key = _first_env("AWS_SECRET_ACCESS_KEY")
130
+ if not aws_access_key_id or not aws_secret_access_key:
131
+ raise unittest.SkipTest(
132
+ "Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY "
133
+ "to download stored files from S3."
134
+ )
135
+
136
+ return cls(
137
+ api_key=api_key,
138
+ base_url=_first_env("BASE_URL") or "https://staging.app.vbase.com",
139
+ s3_bucket=s3_bucket,
140
+ aws_access_key_id=aws_access_key_id,
141
+ aws_secret_access_key=aws_secret_access_key,
142
+ aws_session_token=_first_env("AWS_SESSION_TOKEN"),
143
+ aws_region=_first_env("AWS_REGION", "AWS_DEFAULT_REGION") or "us-east-1",
144
+ timeout=_int_env("FILE_INTEGRITY_E2E_TIMEOUT", 60),
145
+ )
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class FileCase:
150
+ """Source bytes for one integrity scenario."""
151
+
152
+ label: str
153
+ file_name: str
154
+ content: bytes
155
+
156
+ @property
157
+ def object_cid(self) -> str:
158
+ return _sha3_256_cid(self.content)
159
+
160
+
161
+ class VBaseAPIClientFileIntegrityE2ETests(unittest.TestCase):
162
+ """Verify vbase-api-py stamped-file storage preserves exact bytes."""
163
+
164
+ @classmethod
165
+ def setUpClass(cls) -> None:
166
+ _log_and_assert_package_source()
167
+ cls.config = LiveE2EConfig.from_env()
168
+ cls.client = VBaseAPIClient(
169
+ api_key=cls.config.api_key,
170
+ base_url=cls.config.base_url.rstrip("/"),
171
+ timeout=cls.config.timeout,
172
+ )
173
+ cls.s3_client = boto3.session.Session(
174
+ aws_access_key_id=cls.config.aws_access_key_id,
175
+ aws_secret_access_key=cls.config.aws_secret_access_key,
176
+ aws_session_token=cls.config.aws_session_token,
177
+ region_name=cls.config.aws_region,
178
+ ).client("s3")
179
+
180
+ @classmethod
181
+ def tearDownClass(cls) -> None:
182
+ client = getattr(cls, "client", None)
183
+ if client is not None:
184
+ client.close()
185
+
186
+ s3_client = getattr(cls, "s3_client", None)
187
+ if s3_client is not None:
188
+ s3_client.close()
189
+
190
+ @staticmethod
191
+ def _file_cases(prefix: str) -> Iterable[FileCase]:
192
+ suffix = uuid.uuid4().hex
193
+ yield FileCase(
194
+ label="text",
195
+ file_name=f"{prefix}-{suffix}.txt",
196
+ content=f"vbase-api-py {prefix} text payload {suffix}\n".encode("utf-8"),
197
+ )
198
+ yield FileCase(
199
+ label="binary",
200
+ file_name=f"{prefix}-{suffix}.bin",
201
+ content=(
202
+ bytes([0, 1, 2, 3, 4, 127, 128, 254, 255])
203
+ + f" vbase-api-py {prefix} binary payload {suffix} ".encode("utf-8")
204
+ + bytes([255, 0, 64, 32, 16, 8])
205
+ ),
206
+ )
207
+
208
+ def _create_collection(self, name_prefix: str):
209
+ collection_name = f"{name_prefix}-{uuid.uuid4().hex[:12]}"
210
+ deadline = time.monotonic() + _int_env(
211
+ "FILE_INTEGRITY_E2E_API_WAIT_SECONDS", 60
212
+ )
213
+ last_error = None
214
+
215
+ while True:
216
+ try:
217
+ return self.client.create_collection(
218
+ name=collection_name,
219
+ description="vbase-api-py file-integrity E2E collection",
220
+ is_pinned=False,
221
+ )
222
+ except VBaseAPIError as exc:
223
+ if not exc.status_code or exc.status_code < 500:
224
+ raise
225
+ last_error = exc
226
+
227
+ if time.monotonic() >= deadline:
228
+ raise AssertionError(
229
+ f"Timed out creating collection {collection_name}; "
230
+ f"last error: {last_error}"
231
+ ) from last_error
232
+
233
+ time.sleep(5)
234
+
235
+ @staticmethod
236
+ def _write_temp_file(directory: Path, file_case: FileCase) -> Path:
237
+ path = directory / file_case.file_name
238
+ path.write_bytes(file_case.content)
239
+ return path
240
+
241
+ def _download_stored_file(self, file_path: str) -> bytes:
242
+ deadline = time.monotonic() + _int_env(
243
+ "FILE_INTEGRITY_E2E_STORAGE_WAIT_SECONDS", 120
244
+ )
245
+ last_error = None
246
+
247
+ while time.monotonic() < deadline:
248
+ try:
249
+ response = self.s3_client.get_object(
250
+ Bucket=self.config.s3_bucket,
251
+ Key=file_path,
252
+ )
253
+ body = response["Body"]
254
+ try:
255
+ payload = body.read()
256
+ finally:
257
+ body.close()
258
+
259
+ content_length = response.get("ContentLength")
260
+ if content_length is not None and content_length != len(payload):
261
+ raise AssertionError(
262
+ f"S3 ContentLength mismatch for {file_path}: "
263
+ f"metadata={content_length}, bytes={len(payload)}"
264
+ )
265
+ return payload
266
+ except (BotoCoreError, ClientError, KeyError) as exc:
267
+ last_error = str(exc)
268
+
269
+ time.sleep(5)
270
+
271
+ raise AssertionError(
272
+ f"Stored file was not downloadable before timeout: {file_path}; "
273
+ f"last error: {last_error}"
274
+ )
275
+
276
+ def _assert_file_object_path(self, file_path: str, collection_name: str) -> None:
277
+ self.assertIn(
278
+ f"/collections/{collection_name}/stamped/",
279
+ file_path,
280
+ )
281
+
282
+ def _assert_download_integrity(self, file_path: str, file_case: FileCase) -> None:
283
+ downloaded = self._download_stored_file(file_path)
284
+
285
+ self.assertEqual(
286
+ len(downloaded),
287
+ len(file_case.content),
288
+ f"{file_case.label} downloaded byte size",
289
+ )
290
+ self.assertEqual(
291
+ _sha3_256_cid(downloaded),
292
+ file_case.object_cid,
293
+ f"{file_case.label} downloaded SHA3-256",
294
+ )
295
+ self.assertEqual(
296
+ downloaded,
297
+ file_case.content,
298
+ f"{file_case.label} downloaded bytes",
299
+ )
300
+
301
+ def _wait_for_indexed_stamp(self, object_cid: str, collection_cid: str) -> None:
302
+ deadline = time.monotonic() + _int_env(
303
+ "FILE_INTEGRITY_E2E_INDEX_WAIT_SECONDS", 120
304
+ )
305
+ while time.monotonic() < deadline:
306
+ result = self.client.verify_stamps([object_cid], filter_by_user=True)
307
+ for receipt in result.stamp_list:
308
+ if (
309
+ receipt.object_cid.lower() == object_cid.lower()
310
+ and receipt.set_cid.lower() == collection_cid.lower()
311
+ ):
312
+ return
313
+ time.sleep(5)
314
+
315
+ raise AssertionError(
316
+ f"Timed out waiting for indexed stamp: object_cid={object_cid}, "
317
+ f"collection_cid={collection_cid}"
318
+ )
319
+
320
+ def test_create_stamp_with_storage_preserves_downloaded_file_bytes(self):
321
+ """UC-A: create_stamp stores text and binary files without byte drift."""
322
+ with tempfile.TemporaryDirectory() as tmpdir:
323
+ temp_dir = Path(tmpdir)
324
+ for file_case in self._file_cases("create-stamp"):
325
+ with self.subTest(format=file_case.label):
326
+ collection = self._create_collection(
327
+ f"vapi-create-{file_case.label}"
328
+ )
329
+ source_path = self._write_temp_file(temp_dir, file_case)
330
+
331
+ response = self.client.create_stamp(
332
+ file=source_path,
333
+ collection_name=collection.name,
334
+ store_stamped_file=True,
335
+ idempotent=False,
336
+ )
337
+
338
+ self.assertEqual(
339
+ response.commitment_receipt.object_cid.lower(),
340
+ file_case.object_cid.lower(),
341
+ )
342
+ self.assertEqual(
343
+ response.commitment_receipt.set_cid.lower(),
344
+ collection.cid.lower(),
345
+ )
346
+ self.assertIsNotNone(response.file_object)
347
+
348
+ file_path = response.file_object.file_path
349
+ self._assert_file_object_path(file_path, collection.name)
350
+ self._assert_download_integrity(file_path, file_case)
351
+
352
+ def test_upload_stamped_file_preserves_downloaded_file_bytes(self):
353
+ """UC-B: upload_stamped_file stores text and binary files without byte drift."""
354
+ with tempfile.TemporaryDirectory() as tmpdir:
355
+ temp_dir = Path(tmpdir)
356
+ for file_case in self._file_cases("upload-stamped-file"):
357
+ with self.subTest(format=file_case.label):
358
+ collection = self._create_collection(
359
+ f"vapi-upload-{file_case.label}"
360
+ )
361
+ source_path = self._write_temp_file(temp_dir, file_case)
362
+
363
+ stamp = self.client.create_stamp(
364
+ data_cid=file_case.object_cid,
365
+ collection_cid=collection.cid,
366
+ store_stamped_file=False,
367
+ idempotent=False,
368
+ )
369
+ self.assertEqual(
370
+ stamp.commitment_receipt.object_cid.lower(),
371
+ file_case.object_cid.lower(),
372
+ )
373
+ self.assertEqual(
374
+ stamp.commitment_receipt.set_cid.lower(),
375
+ collection.cid.lower(),
376
+ )
377
+ self._wait_for_indexed_stamp(file_case.object_cid, collection.cid)
378
+
379
+ upload = self.client.upload_stamped_file(
380
+ collection_name=collection.name,
381
+ file=source_path,
382
+ )
383
+
384
+ self.assertEqual(
385
+ upload.commitment_receipt.object_cid.lower(),
386
+ file_case.object_cid.lower(),
387
+ )
388
+ self.assertEqual(
389
+ upload.commitment_receipt.set_cid.lower(),
390
+ collection.cid.lower(),
391
+ )
392
+ self.assertIsNotNone(upload.file_object)
393
+
394
+ file_path = upload.file_object.file_path
395
+ self._assert_file_object_path(file_path, collection.name)
396
+ self._assert_download_integrity(file_path, file_case)
397
+
398
+
399
+ if __name__ == "__main__":
400
+ unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "0.1.2"
@@ -1 +0,0 @@
1
- requests>=2.7.0
File without changes
File without changes
File without changes