vautra-sdk 1.0.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.
Files changed (35) hide show
  1. vautra_sdk-1.0.0/CHANGELOG.md +89 -0
  2. vautra_sdk-1.0.0/LICENSE +21 -0
  3. vautra_sdk-1.0.0/MANIFEST.in +10 -0
  4. vautra_sdk-1.0.0/PKG-INFO +348 -0
  5. vautra_sdk-1.0.0/README.md +307 -0
  6. vautra_sdk-1.0.0/pyproject.toml +92 -0
  7. vautra_sdk-1.0.0/setup.cfg +4 -0
  8. vautra_sdk-1.0.0/src/vautra/__init__.py +43 -0
  9. vautra_sdk-1.0.0/src/vautra/_http.py +411 -0
  10. vautra_sdk-1.0.0/src/vautra/_signer.py +101 -0
  11. vautra_sdk-1.0.0/src/vautra/_version.py +1 -0
  12. vautra_sdk-1.0.0/src/vautra/client.py +183 -0
  13. vautra_sdk-1.0.0/src/vautra/errors.py +116 -0
  14. vautra_sdk-1.0.0/src/vautra/py.typed +0 -0
  15. vautra_sdk-1.0.0/src/vautra/resources/__init__.py +13 -0
  16. vautra_sdk-1.0.0/src/vautra/resources/_params.py +43 -0
  17. vautra_sdk-1.0.0/src/vautra/resources/buckets.py +100 -0
  18. vautra_sdk-1.0.0/src/vautra/resources/objects.py +320 -0
  19. vautra_sdk-1.0.0/src/vautra/resources/projects.py +62 -0
  20. vautra_sdk-1.0.0/src/vautra/types.py +231 -0
  21. vautra_sdk-1.0.0/src/vautra/uploads/__init__.py +6 -0
  22. vautra_sdk-1.0.0/src/vautra/uploads/body.py +229 -0
  23. vautra_sdk-1.0.0/src/vautra/uploads/multipart.py +375 -0
  24. vautra_sdk-1.0.0/src/vautra/uploads/part_queue.py +136 -0
  25. vautra_sdk-1.0.0/src/vautra_sdk.egg-info/PKG-INFO +348 -0
  26. vautra_sdk-1.0.0/src/vautra_sdk.egg-info/SOURCES.txt +33 -0
  27. vautra_sdk-1.0.0/src/vautra_sdk.egg-info/dependency_links.txt +1 -0
  28. vautra_sdk-1.0.0/src/vautra_sdk.egg-info/requires.txt +10 -0
  29. vautra_sdk-1.0.0/src/vautra_sdk.egg-info/top_level.txt +1 -0
  30. vautra_sdk-1.0.0/tests/__init__.py +0 -0
  31. vautra_sdk-1.0.0/tests/conftest.py +42 -0
  32. vautra_sdk-1.0.0/tests/mock_server.py +457 -0
  33. vautra_sdk-1.0.0/tests/test_client.py +251 -0
  34. vautra_sdk-1.0.0/tests/test_download.py +93 -0
  35. vautra_sdk-1.0.0/tests/test_upload.py +359 -0
@@ -0,0 +1,89 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## 1.0.0
7
+
8
+ First published release. The package is distributed on PyPI as `vautra-sdk` and
9
+ imported as `vautra`.
10
+
11
+ The pre-release layout installed the top-level modules `client`, `models`,
12
+ `resources`, `uploads` and `vautra` into site-packages, which would have
13
+ collided with unrelated packages on any machine that installed it. Everything
14
+ now lives under a single `vautra` package.
15
+
16
+ ### Security
17
+
18
+ - Redirects are never followed. Previously `requests` followed them by default,
19
+ which replayed signed credential headers to whatever host a `Location` header
20
+ named. An unexpected `3xx` now raises `VautraError` with code
21
+ `unexpected_redirect`.
22
+ - `api_url` must use `https://` unless it points at a loopback address, so
23
+ signed credentials cannot be sent over plaintext. Malformed URLs, wrong types,
24
+ and empty credentials raise `VautraConfigError` before any request is sent.
25
+ - `Vautra`, `VautraConfig` and the transport redact the secret access key in
26
+ `repr()`, so clients can be logged safely.
27
+ - The connection adapter is pinned to zero transport-level retries; a urllib3
28
+ replay would have reused a nonce, which the service rejects.
29
+ - Requests are signed against the URL `requests` actually puts on the wire, so
30
+ escaping differences cannot silently invalidate a signature.
31
+
32
+ ### Fixed
33
+
34
+ - Requests now default to a 30 second timeout. Without one, an unresponsive
35
+ server hung the caller forever.
36
+ - `objects.delete()` and `objects.cancel_upload()` return the service message.
37
+ Responses that carry only a message and no `data` key were being rewrapped,
38
+ which discarded it.
39
+ - Boolean query parameters are sent as `true`/`false` rather than Python's
40
+ `True`/`False`.
41
+ - The server's part plan is validated against the body size. An inconsistent
42
+ plan previously uploaded a truncated object and still reported success.
43
+ - Object keys are validated locally before an upload session is created.
44
+ - Non-seekable streams are spooled to a temporary file instead of being read
45
+ entirely into memory, so memory stays flat and failed parts stay retryable.
46
+ - Locally raised errors (unreadable body, bad plan, cancellation) are marked
47
+ non-retryable instead of consuming the retry budget.
48
+ - A failing part now stops its sibling workers instead of letting them run on.
49
+ - Progress counters are updated under a lock and count each part once, so a
50
+ retried part cannot push `loadedBytes` past the total.
51
+ - `mimeType` is omitted when the caller does not pass `content_type`, letting
52
+ Vautra derive it from the extension instead of storing
53
+ `application/octet-stream`.
54
+ - Buffered downloads are capped at 256 MiB (`max_bytes=0` disables the cap),
55
+ enforced against both `Content-Length` and the bytes actually received.
56
+ - `download(destination=...)` writes to a temporary sibling and renames it into
57
+ place, so an interrupted download leaves no truncated file.
58
+ - Path segments are percent-encoded and validated on every method.
59
+
60
+ ### Changed
61
+
62
+ - The API is snake_case throughout; the camelCase parameter and method aliases
63
+ are gone.
64
+ - Timeouts are expressed in seconds (`timeout`, `attempt_timeout`) rather than a
65
+ mix of seconds and milliseconds.
66
+ - Cancellation is expressed with a `threading.Event` passed as `cancel_event`.
67
+ - `download_stream()` returns a `DownloadStream` context manager that iterates
68
+ chunks, rather than a raw `requests.Response`.
69
+ - Credentials and `api_url` fall back to `VAUTRA_ACCESS_KEY_ID`,
70
+ `VAUTRA_SECRET_ACCESS_KEY` and `VAUTRA_API_URL`.
71
+ - `Vautra` is a context manager and exposes `close()`.
72
+ - Error types are specific: `VautraConfigError`, `VautraTimeoutError`,
73
+ `VautraConnectionError`, `VautraCancelledError`, all deriving from
74
+ `VautraError`.
75
+
76
+ ### Removed
77
+
78
+ - `objects.get()` and `objects.public_link()`. Neither route exists on the
79
+ Vautra API, so both could only ever return `404`.
80
+ - `client.access_keys`, whose single method raised unconditionally. Access keys
81
+ are created in the dashboard, where the secret can be shown once.
82
+
83
+ ### Added
84
+
85
+ - `py.typed`: the package ships inline type information.
86
+ - A test suite that runs against a mock Vautra API porting the backend's
87
+ signature verifier.
88
+ - CI running the tests on Python 3.9 through 3.13, plus `ruff` and strict
89
+ `mypy`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vautra Technologies Inc.
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,10 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include src/vautra/py.typed
5
+ recursive-include tests *.py
6
+ prune .venv
7
+ prune dist
8
+ prune build
9
+ global-exclude *.py[cod]
10
+ global-exclude .env
@@ -0,0 +1,348 @@
1
+ Metadata-Version: 2.4
2
+ Name: vautra-sdk
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for Vautra projects, buckets and VS3 object storage.
5
+ Author: Vautra Technologies Inc.
6
+ Maintainer: Vautra Technologies Inc.
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/KWP-inc/Vautra-python-sdk
9
+ Project-URL: Documentation, https://core-docs.vautra.com
10
+ Project-URL: Repository, https://github.com/KWP-inc/Vautra-python-sdk
11
+ Project-URL: Issues, https://github.com/KWP-inc/Vautra-python-sdk/issues
12
+ Project-URL: Changelog, https://github.com/KWP-inc/Vautra-python-sdk/blob/main/CHANGELOG.md
13
+ Keywords: vautra,vs3,object-storage,storage,sdk,upload
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3 :: Only
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Topic :: System :: Archiving
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: requests>=2.31.0
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8.0; extra == "dev"
34
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
35
+ Requires-Dist: mypy>=1.10; extra == "dev"
36
+ Requires-Dist: ruff>=0.5; extra == "dev"
37
+ Requires-Dist: types-requests>=2.31; extra == "dev"
38
+ Requires-Dist: build>=1.2; extra == "dev"
39
+ Requires-Dist: twine>=5.0; extra == "dev"
40
+ Dynamic: license-file
41
+
42
+ # Vautra Python SDK
43
+
44
+ Python SDK for Vautra projects, buckets, and object storage.
45
+
46
+ The SDK signs every request and sends it to `https://app.vautra.com/api` by default. Keep the secret access key on a trusted server; it is a credential, not a public identifier.
47
+
48
+ ## Requirements
49
+
50
+ - Python 3.9 or newer.
51
+ - `requests` 2.31 or newer (installed automatically).
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install vautra-sdk
57
+ ```
58
+
59
+ The distribution is `vautra-sdk`; the import name is `vautra`. The npm package of
60
+ the same name is the separate Node.js SDK.
61
+
62
+ ## Authentication
63
+
64
+ Create an access key in the Vautra dashboard, then build one client and reuse it — each client owns a connection pool:
65
+
66
+ ```python
67
+ from vautra import Vautra
68
+
69
+ vautra = Vautra(
70
+ access_key_id="...",
71
+ secret_access_key="...",
72
+ )
73
+ ```
74
+
75
+ Credentials fall back to the `VAUTRA_ACCESS_KEY_ID` and `VAUTRA_SECRET_ACCESS_KEY` environment variables, and `api_url` to `VAUTRA_API_URL`, when the corresponding argument is omitted:
76
+
77
+ ```python
78
+ from vautra import Vautra
79
+
80
+ with Vautra() as vautra: # reads the environment
81
+ page = vautra.projects.list()
82
+ ```
83
+
84
+ Every request carries a fresh nonce, including retries, so signed headers are never replayed. Access-key permissions and project or bucket restrictions are enforced by the Vautra backend.
85
+
86
+ Invalid configuration raises `VautraConfigError` immediately, before any request is sent. `api_url` must use `https://` unless it points at a loopback address, so signed credentials never travel over plaintext.
87
+
88
+ ### Client options
89
+
90
+ | Option | Default | Description |
91
+ | --- | --- | --- |
92
+ | `access_key_id` | `$VAUTRA_ACCESS_KEY_ID` | Required. |
93
+ | `secret_access_key` | `$VAUTRA_SECRET_ACCESS_KEY` | Required. Never logged; the client redacts it in `repr()`. |
94
+ | `api_url` | `$VAUTRA_API_URL` or `https://app.vautra.com/api` | Must be `https://` for remote hosts. |
95
+ | `timeout` | `30.0` | Seconds, for non-upload requests. Use `0` or `None` to disable. |
96
+ | `session` | new session | Supply your own `requests.Session` to control proxies or TLS verification. |
97
+
98
+ Every method also accepts a per-call `timeout=` override and a `cancel_event=` (a `threading.Event`).
99
+
100
+ `Vautra` is a context manager, and `close()` releases the connection pool:
101
+
102
+ ```python
103
+ with Vautra(access_key_id="...", secret_access_key="...") as vautra:
104
+ ...
105
+ ```
106
+
107
+ ## Object keys
108
+
109
+ Vautra object keys are **flat file names**, not S3-style paths:
110
+
111
+ - No `/` or `\` separators — `documents/report.pdf` is rejected.
112
+ - Maximum 255 bytes (UTF-8), no control characters, no trailing period.
113
+ - The key must end in a supported extension.
114
+
115
+ Supported extensions are currently: `txt`, `csv`, `json`, `xml`, `html`, `css`, `js`, `ts`, `md`, `pdf`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `svg`, `zip`, `gz`, `doc`, `docx`, `xlsx`, `pptx`, `mp3`, `mp4`, `webm`, `wav`, `ogg`. Files whose contents do not match their extension are rejected on upload.
116
+
117
+ Invalid keys raise `VautraError` immediately, before an upload session is created.
118
+
119
+ ## Projects
120
+
121
+ ```python
122
+ page = vautra.projects.list(
123
+ page=1,
124
+ page_size=20,
125
+ search="production",
126
+ key_management="managed",
127
+ )
128
+
129
+ project = vautra.projects.get("project-id")
130
+ ```
131
+
132
+ List methods return `{"data": [...], "meta": {"page", "pageSize", "total", "totalPages"}}`.
133
+
134
+ ## Buckets
135
+
136
+ ```python
137
+ page = vautra.buckets.list(project_id="project-id", page=1, page_size=20)
138
+
139
+ bucket = vautra.buckets.get("bucket-id")
140
+
141
+ created = vautra.buckets.create(
142
+ project_id="project-id",
143
+ name="documents",
144
+ versioning_enabled=True,
145
+ )
146
+ ```
147
+
148
+ Bucket deletion is intentionally not exposed by the SDK.
149
+
150
+ ## List objects
151
+
152
+ ```python
153
+ page = vautra.objects.list("bucket-id", page=1, page_size=20, search="invoice")
154
+ ```
155
+
156
+ ## Upload objects
157
+
158
+ For large local files, pass a path. The SDK reads only the active chunks instead of loading the whole file into memory:
159
+
160
+ ```python
161
+ obj = vautra.objects.upload(
162
+ bucket_id="bucket-id",
163
+ key="movie.mp4",
164
+ body="C:/videos/movie.mp4", # str, pathlib.Path, or {"path": ...}
165
+ content_type="video/mp4",
166
+ )
167
+ ```
168
+
169
+ For content already in memory:
170
+
171
+ ```python
172
+ vautra.objects.upload(
173
+ bucket_id="bucket-id",
174
+ key="hello.txt",
175
+ body=b"Hello from Vautra",
176
+ content_type="text/plain",
177
+ )
178
+ ```
179
+
180
+ When `content_type` is omitted, Vautra derives the stored MIME type from the key's extension.
181
+
182
+ ### Supported body types
183
+
184
+ | Body | Behaviour |
185
+ | --- | --- |
186
+ | `bytes`, `bytearray`, `memoryview` | Used directly. |
187
+ | `str` | Encoded as UTF-8. |
188
+ | `pathlib.Path`, `os.PathLike`, `{"path": "..."}` | Opened and read by range; memory stays flat. |
189
+ | A seekable binary file object | Read by range from its current position. The SDK does not close a handle it did not open. |
190
+ | A non-seekable stream (pipe, socket, generator of `bytes`) | Spooled to a temporary file so failed parts can be retried, then cleaned up. |
191
+ | Any object with `size` and `read(start, end)` | Used as a random-access source. |
192
+
193
+ A stream cannot be rewound, so a failed part could not otherwise be retried. Spooling keeps memory flat regardless of stream size. Prefer a path when the data is already on disk.
194
+
195
+ Text-mode file objects are rejected — open files with `"rb"`.
196
+
197
+ ### Upload options
198
+
199
+ ```python
200
+ import threading
201
+
202
+ cancel = threading.Event()
203
+
204
+ vautra.objects.upload(
205
+ bucket_id="bucket-id",
206
+ key="archive.zip",
207
+ body="./archive.zip",
208
+ concurrency=2, # 1-4
209
+ max_attempts=3, # 1-5
210
+ attempt_timeout=180.0, # seconds per request
211
+ cancel_event=cancel,
212
+ on_started=lambda upload_id: print("session", upload_id),
213
+ on_progress=lambda p: print(p["stage"], p["loadedBytes"], p["totalBytes"]),
214
+ )
215
+ ```
216
+
217
+ Progress stages are `starting`, `uploading`, `finalizing`, and `success`. `loaded_bytes` never goes backwards and a retried part is counted once.
218
+
219
+ Part and completion requests retry network failures, HTTP `408`, `429`, and `5xx`. Retry delays are one second then three seconds. Other `4xx` responses are terminal, as are errors raised locally by the SDK.
220
+
221
+ If an upload fails after its session is created, the SDK makes a best-effort request to cancel and clean up that session. Secure cross-process resume is not supported.
222
+
223
+ SDK access keys work only with Vautra-managed projects and their buckets. Customer-managed projects are intentionally excluded from SDK access-key scopes.
224
+
225
+ ## Upload status and cancellation
226
+
227
+ ```python
228
+ status = vautra.objects.upload_status("upload-id")
229
+ vautra.objects.cancel_upload("upload-id")
230
+ ```
231
+
232
+ `upload_status()` is for inspection only; it does not enable resume.
233
+
234
+ To cancel a running upload, set the `threading.Event` you passed as `cancel_event`:
235
+
236
+ ```python
237
+ import threading
238
+
239
+ cancel = threading.Event()
240
+ worker = threading.Thread(
241
+ target=vautra.objects.upload,
242
+ kwargs={
243
+ "bucket_id": "bucket-id",
244
+ "key": "large.zip",
245
+ "body": "./large.zip",
246
+ "cancel_event": cancel,
247
+ },
248
+ )
249
+ worker.start()
250
+ cancel.set()
251
+ ```
252
+
253
+ The upload raises `VautraCancelledError`, and the session is cleaned up. An already-set event raises before any request is sent.
254
+
255
+ Because `requests` is synchronous, cancellation takes effect between parts and between retry attempts — it does not interrupt a request already on the wire. `attempt_timeout` bounds that window.
256
+
257
+ ## Download objects
258
+
259
+ Return the object as `bytes`:
260
+
261
+ ```python
262
+ data = vautra.objects.download("object-id")
263
+ ```
264
+
265
+ Buffered downloads are capped at 256 MiB so a large object cannot exhaust memory. Pass `max_bytes` to change the cap, or `0` to disable it. Objects above the cap raise `VautraError` with status `413`.
266
+
267
+ Write directly to a file, which streams and is not capped:
268
+
269
+ ```python
270
+ vautra.objects.download("object-id", destination="./report.pdf")
271
+ ```
272
+
273
+ The file is written to a temporary sibling and renamed into place, so an interrupted download never leaves a truncated file at `destination`.
274
+
275
+ Or consume the stream yourself:
276
+
277
+ ```python
278
+ with vautra.objects.download_stream("object-id") as stream:
279
+ print(stream.content_type, stream.content_length)
280
+ for chunk in stream:
281
+ sink.write(chunk)
282
+ ```
283
+
284
+ Downloads through the SDK are available only for Vautra-managed projects.
285
+
286
+ ## Delete objects
287
+
288
+ ```python
289
+ vautra.objects.delete("object-id")
290
+ ```
291
+
292
+ Deletion is subject to access-key permissions and backend object-state rules.
293
+
294
+ ## Errors
295
+
296
+ Every SDK error derives from `VautraError`:
297
+
298
+ ```python
299
+ from vautra import VautraError
300
+
301
+ try:
302
+ vautra.buckets.get("missing-bucket")
303
+ except VautraError as error:
304
+ print(error.status, error.code, error.message)
305
+ ```
306
+
307
+ | Exception | Raised when |
308
+ | --- | --- |
309
+ | `VautraError` | The API returned a failure. Carries `status`, `code`, `details`. |
310
+ | `VautraConfigError` | Client configuration is unusable. Raised before any request. |
311
+ | `VautraTimeoutError` | A request exceeded its time budget (`status` 408). |
312
+ | `VautraConnectionError` | The API could not be reached (`status` 0). |
313
+ | `VautraCancelledError` | The caller's `cancel_event` was set. |
314
+
315
+ Errors also expose `retryable`, which the upload retry loop uses to avoid burning attempts on failures that would repeat identically.
316
+
317
+ The SDK never follows redirects: signed credentials are not forwarded to another host, and an unexpected `3xx` surfaces as a `VautraError` with code `unexpected_redirect`.
318
+
319
+ ## Thread safety
320
+
321
+ A `Vautra` client is safe to share across threads. Uploads use a bounded worker pool internally and read bodies under a lock.
322
+
323
+ ## Development
324
+
325
+ ```bash
326
+ python -m venv .venv && .venv/Scripts/activate # or source .venv/bin/activate
327
+ pip install -e ".[dev]"
328
+ ```
329
+
330
+ ```bash
331
+ pytest
332
+ ```
333
+
334
+ ```bash
335
+ ruff check . && mypy
336
+ ```
337
+
338
+ The test suite runs against a mock Vautra API whose signature verification is a port of the backend's verifier, so a passing test means the SDK interoperates with the real service rather than with a permissive stub.
339
+
340
+ To try a real upload:
341
+
342
+ ```bash
343
+ python examples/upload_file.py ./examples/sample.txt --bucket-id BUCKET --key sample.txt
344
+ ```
345
+
346
+ ## License
347
+
348
+ MIT. See [LICENSE](LICENSE).