wherobots-python-sdk 0.4.0__tar.gz → 0.5.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 (44) hide show
  1. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/PKG-INFO +49 -2
  2. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/README.md +48 -1
  3. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/pyproject.toml +1 -1
  4. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_base_client.py +102 -0
  5. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_client_header.py +3 -3
  6. wherobots_python_sdk-0.5.0/tests/test_files_drive.py +811 -0
  7. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_init_exports.py +18 -0
  8. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/__init__.py +16 -1
  9. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/__version__.py +1 -1
  10. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/api/__init__.py +2 -1
  11. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/api/base.py +81 -24
  12. wherobots_python_sdk-0.5.0/wherobots/api/drive.py +481 -0
  13. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/enums.py +10 -0
  14. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/exceptions.py +34 -0
  15. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/models.py +130 -0
  16. wherobots_python_sdk-0.5.0/wherobots/sdk.py +91 -0
  17. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots_python_sdk.egg-info/PKG-INFO +49 -2
  18. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots_python_sdk.egg-info/SOURCES.txt +3 -0
  19. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/LICENSE +0 -0
  20. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/setup.cfg +0 -0
  21. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_api.py +0 -0
  22. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_client.py +0 -0
  23. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_config.py +0 -0
  24. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_enums.py +0 -0
  25. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_exceptions.py +0 -0
  26. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_files_api.py +0 -0
  27. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_integration.py +0 -0
  28. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_logger.py +0 -0
  29. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_models.py +0 -0
  30. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_regressions.py +0 -0
  31. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_security.py +0 -0
  32. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/tests/test_utils.py +0 -0
  33. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/api/files.py +0 -0
  34. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/api/runs.py +0 -0
  35. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/client.py +0 -0
  36. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/config.py +0 -0
  37. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/py.typed +0 -0
  38. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/utils/__init__.py +0 -0
  39. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/utils/client_header.py +0 -0
  40. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/utils/logger.py +0 -0
  41. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots/utils/validation.py +0 -0
  42. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots_python_sdk.egg-info/dependency_links.txt +0 -0
  43. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots_python_sdk.egg-info/requires.txt +0 -0
  44. {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.0}/wherobots_python_sdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wherobots-python-sdk
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Python SDK for Wherobots (currently covers the Jobs REST API)
5
5
  Author-email: Wherobots <support@wherobots.com>
6
6
  License-Expression: Apache-2.0
@@ -45,6 +45,7 @@ Additional Wherobots surfaces will be added over time.
45
45
  - List runs with status, name, and date filters
46
46
  - Typed dataclass models for all API responses (no pydantic dependency)
47
47
  - Environment-based configuration
48
+ - Files: list, create folders, upload, download, rename and delete in your Wherobots file area
48
49
 
49
50
  ## Installation
50
51
 
@@ -418,6 +419,49 @@ job = WherobotsJob(
418
419
  )
419
420
  ```
420
421
 
422
+ ## Files
423
+
424
+ `Files` is your Wherobots file area ("My Files" in Studio) from Python. Start from a
425
+ `Wherobots` client; it reads `WHEROBOTS_API_KEY` and `WHEROBOTS_REGION`, or takes `api_key=` /
426
+ `region=` overrides, and closes its connection as a context manager.
427
+
428
+ ```python
429
+ import wherobots
430
+ from wherobots import Drive, Region
431
+
432
+ with wherobots.Wherobots() as wb:
433
+ drive = wb.files.my_files # same object as wb.files.drive(Drive.MY_FILES)
434
+ eu = wb.files.drive(Drive.MY_FILES, region=Region.EU_WEST_1) # another region
435
+
436
+ drive.make_directory("projects/2026/q3") # creates projects, then projects/2026, then .../q3
437
+ drive.upload_file("report.csv", "projects/2026/q3/report.csv")
438
+ for entry in drive.list_directory("projects/2026/q3"): # "" lists the root
439
+ print(entry.name, entry.is_dir, entry.size, entry.last_modified)
440
+ drive.download_file("projects/2026/q3/report.csv", "local-copy.csv")
441
+ drive.rename_file("projects/2026/q3/report.csv", "final.csv")
442
+ drive.delete_file("projects/2026/q3/final.csv")
443
+ drive.delete_directory("projects")
444
+ ```
445
+
446
+ - Each path segment is encoded on its own; empty levels, `.` and `..` are refused before any
447
+ request.
448
+ - Uploads and downloads go straight to storage through short-lived presigned URLs. The API key
449
+ is never sent to storage, and no storage credentials are requested.
450
+ - Uploads are capped at 500 MiB per file, matching the CLI and the Studio web UI; a larger file
451
+ is refused before any request is sent.
452
+ - Listing follows the server's pages until the end and returns every entry.
453
+ - A region is required: with none passed or configured, `wb.files.my_files` raises
454
+ `WherobotsConfigError`.
455
+
456
+ | Failure | Raised as |
457
+ |---|---|
458
+ | Files is not enabled for this drive in this region | `WherobotsFilesNotEnabledError` (`.drive`, `.region`) |
459
+ | The API key was not accepted | `WherobotsNotAuthenticatedError` |
460
+ | No such file or folder | `WherobotsFileNotFoundError` |
461
+ | Transfer to or from storage failed | `WherobotsS3Error` |
462
+
463
+ The three `Wherobots*Error` classes above subclass `WherobotsAPIError`.
464
+
421
465
  ## Enums
422
466
 
423
467
  ### JobStatus
@@ -585,9 +629,12 @@ All exceptions inherit from `WherobotsJobError`.
585
629
  | `WherobotsJobError` | Base exception for all SDK errors. |
586
630
  | `WherobotsAPIError` | API returned an error response. Has `status_code`, `response`, `request_id` attributes. |
587
631
  | `WherobotsValidationError` | Client-side validation failed (e.g., invalid name). |
588
- | `WherobotsS3Error` | S3 upload or access operation failed. Deprecated — presigned uploads raise `WherobotsAPIError` instead. |
632
+ | `WherobotsS3Error` | A transfer to or from storage failed, including Files presigned uploads and downloads. |
589
633
  | `WherobotsConfigError` | Configuration is missing or invalid. |
590
634
  | `WherobotsTimeoutError` | Job exceeded the specified timeout. |
635
+ | `WherobotsNotAuthenticatedError` | Files: the API key was not accepted (HTTP 401). Subclass of `WherobotsAPIError`. |
636
+ | `WherobotsFilesNotEnabledError` | Files is not enabled for a drive in a region. Has `drive`, `region`. Subclass of `WherobotsAPIError`. |
637
+ | `WherobotsFileNotFoundError` | Files: no such file or folder. Subclass of `WherobotsAPIError`. |
591
638
 
592
639
  ```python
593
640
  from wherobots import WherobotsAPIError, WherobotsJobError
@@ -18,6 +18,7 @@ Additional Wherobots surfaces will be added over time.
18
18
  - List runs with status, name, and date filters
19
19
  - Typed dataclass models for all API responses (no pydantic dependency)
20
20
  - Environment-based configuration
21
+ - Files: list, create folders, upload, download, rename and delete in your Wherobots file area
21
22
 
22
23
  ## Installation
23
24
 
@@ -391,6 +392,49 @@ job = WherobotsJob(
391
392
  )
392
393
  ```
393
394
 
395
+ ## Files
396
+
397
+ `Files` is your Wherobots file area ("My Files" in Studio) from Python. Start from a
398
+ `Wherobots` client; it reads `WHEROBOTS_API_KEY` and `WHEROBOTS_REGION`, or takes `api_key=` /
399
+ `region=` overrides, and closes its connection as a context manager.
400
+
401
+ ```python
402
+ import wherobots
403
+ from wherobots import Drive, Region
404
+
405
+ with wherobots.Wherobots() as wb:
406
+ drive = wb.files.my_files # same object as wb.files.drive(Drive.MY_FILES)
407
+ eu = wb.files.drive(Drive.MY_FILES, region=Region.EU_WEST_1) # another region
408
+
409
+ drive.make_directory("projects/2026/q3") # creates projects, then projects/2026, then .../q3
410
+ drive.upload_file("report.csv", "projects/2026/q3/report.csv")
411
+ for entry in drive.list_directory("projects/2026/q3"): # "" lists the root
412
+ print(entry.name, entry.is_dir, entry.size, entry.last_modified)
413
+ drive.download_file("projects/2026/q3/report.csv", "local-copy.csv")
414
+ drive.rename_file("projects/2026/q3/report.csv", "final.csv")
415
+ drive.delete_file("projects/2026/q3/final.csv")
416
+ drive.delete_directory("projects")
417
+ ```
418
+
419
+ - Each path segment is encoded on its own; empty levels, `.` and `..` are refused before any
420
+ request.
421
+ - Uploads and downloads go straight to storage through short-lived presigned URLs. The API key
422
+ is never sent to storage, and no storage credentials are requested.
423
+ - Uploads are capped at 500 MiB per file, matching the CLI and the Studio web UI; a larger file
424
+ is refused before any request is sent.
425
+ - Listing follows the server's pages until the end and returns every entry.
426
+ - A region is required: with none passed or configured, `wb.files.my_files` raises
427
+ `WherobotsConfigError`.
428
+
429
+ | Failure | Raised as |
430
+ |---|---|
431
+ | Files is not enabled for this drive in this region | `WherobotsFilesNotEnabledError` (`.drive`, `.region`) |
432
+ | The API key was not accepted | `WherobotsNotAuthenticatedError` |
433
+ | No such file or folder | `WherobotsFileNotFoundError` |
434
+ | Transfer to or from storage failed | `WherobotsS3Error` |
435
+
436
+ The three `Wherobots*Error` classes above subclass `WherobotsAPIError`.
437
+
394
438
  ## Enums
395
439
 
396
440
  ### JobStatus
@@ -558,9 +602,12 @@ All exceptions inherit from `WherobotsJobError`.
558
602
  | `WherobotsJobError` | Base exception for all SDK errors. |
559
603
  | `WherobotsAPIError` | API returned an error response. Has `status_code`, `response`, `request_id` attributes. |
560
604
  | `WherobotsValidationError` | Client-side validation failed (e.g., invalid name). |
561
- | `WherobotsS3Error` | S3 upload or access operation failed. Deprecated — presigned uploads raise `WherobotsAPIError` instead. |
605
+ | `WherobotsS3Error` | A transfer to or from storage failed, including Files presigned uploads and downloads. |
562
606
  | `WherobotsConfigError` | Configuration is missing or invalid. |
563
607
  | `WherobotsTimeoutError` | Job exceeded the specified timeout. |
608
+ | `WherobotsNotAuthenticatedError` | Files: the API key was not accepted (HTTP 401). Subclass of `WherobotsAPIError`. |
609
+ | `WherobotsFilesNotEnabledError` | Files is not enabled for a drive in a region. Has `drive`, `region`. Subclass of `WherobotsAPIError`. |
610
+ | `WherobotsFileNotFoundError` | Files: no such file or folder. Subclass of `WherobotsAPIError`. |
564
611
 
565
612
  ```python
566
613
  from wherobots import WherobotsAPIError, WherobotsJobError
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools>=45", "wheel"]
2
+ requires = ["setuptools>=83.0.0", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
@@ -233,6 +233,108 @@ class TestRequestErrorWrapping:
233
233
  assert "Internal Server Error" in str(exc_info.value)
234
234
 
235
235
 
236
+ # ---------------------------------------------------------------------------
237
+ # Redirect-returning GET
238
+ # ---------------------------------------------------------------------------
239
+
240
+
241
+ def _mock_session(MockSession):
242
+ session = MockSession.return_value
243
+ session.headers = {}
244
+ session.mount = MagicMock()
245
+ return session
246
+
247
+
248
+ class TestGetRedirectTarget:
249
+ @patch("wherobots.api.base.requests.Session")
250
+ def test_returns_location_of_307_without_following(self, MockSession, config):
251
+ session = _mock_session(MockSession)
252
+ response = MagicMock()
253
+ response.status_code = 307
254
+ response.headers = {"Location": "https://s3.example.com/obj?sig=1"}
255
+ response.raise_for_status.return_value = None
256
+ session.request.return_value = response
257
+
258
+ client = BaseClient(config)
259
+ target = client.get_redirect_target("/storage/x/files/a", params={"q": "1"})
260
+
261
+ assert target == "https://s3.example.com/obj?sig=1"
262
+ call = session.request.call_args
263
+ assert call.args == ("GET", "https://api.test.wherobots.com/storage/x/files/a")
264
+ assert call.kwargs["allow_redirects"] is False
265
+ assert call.kwargs["params"] == {"q": "1"}
266
+ assert call.kwargs["timeout"] == 30
267
+
268
+ @patch("wherobots.api.base.requests.Session")
269
+ def test_non_redirect_answer_is_api_error(self, MockSession, config):
270
+ session = _mock_session(MockSession)
271
+ response = MagicMock()
272
+ response.status_code = 200
273
+ response.headers = {"Location": "https://s3.example.com/obj"}
274
+ response.raise_for_status.return_value = None
275
+ session.request.return_value = response
276
+
277
+ with pytest.raises(WherobotsAPIError, match="Expected a redirect") as exc_info:
278
+ BaseClient(config).get_redirect_target("/storage/x/files/a")
279
+ assert exc_info.value.status_code == 200
280
+
281
+ @patch("wherobots.api.base.requests.Session")
282
+ def test_redirect_without_location_is_api_error(self, MockSession, config):
283
+ session = _mock_session(MockSession)
284
+ response = MagicMock()
285
+ response.status_code = 307
286
+ response.headers = {}
287
+ response.raise_for_status.return_value = None
288
+ session.request.return_value = response
289
+
290
+ with pytest.raises(WherobotsAPIError, match="Expected a redirect"):
291
+ BaseClient(config).get_redirect_target("/storage/x/files/a")
292
+
293
+ @patch("wherobots.api.base.requests.Session")
294
+ def test_http_error_mapped_like_request(self, MockSession, config):
295
+ session = _mock_session(MockSession)
296
+ response = MagicMock()
297
+ response.status_code = 404
298
+ response.headers = {"X-Request-Id": "req-xyz"}
299
+ response.json.return_value = {"message": "nope"}
300
+ response.raise_for_status.side_effect = requests.HTTPError(response=response)
301
+ session.request.return_value = response
302
+
303
+ with pytest.raises(WherobotsAPIError) as exc_info:
304
+ BaseClient(config).get_redirect_target("/storage/x/files/a")
305
+ err = exc_info.value
306
+ assert err.status_code == 404
307
+ assert err.request_id == "req-xyz"
308
+ assert err.response == {"message": "nope"}
309
+ assert "nope" in str(err)
310
+
311
+ @patch("wherobots.api.base.requests.Session")
312
+ def test_connection_error_wrapped(self, MockSession, config):
313
+ session = _mock_session(MockSession)
314
+ session.request.side_effect = requests.ConnectionError("DNS failed")
315
+
316
+ with pytest.raises(WherobotsAPIError, match="Request failed"):
317
+ BaseClient(config).get_redirect_target("/storage/x/files/a")
318
+
319
+ @patch("wherobots.api.base.requests.Session")
320
+ def test_other_calls_still_refuse_redirects(self, MockSession, config):
321
+ session = _mock_session(MockSession)
322
+ session.request.return_value = MagicMock()
323
+
324
+ client = BaseClient(config)
325
+ client.get("/runs")
326
+ client.post("/runs", json_body={})
327
+
328
+ assert session.max_redirects == 0
329
+ for call in session.request.call_args_list:
330
+ assert "allow_redirects" not in call.kwargs
331
+
332
+ @patch("wherobots.api.base.requests.Session")
333
+ def test_timeout_property(self, MockSession, config):
334
+ _mock_session(MockSession)
335
+ assert BaseClient(config).timeout == 30
336
+
337
+
236
338
  # ---------------------------------------------------------------------------
237
339
  # Convenience wrappers
238
340
  # ---------------------------------------------------------------------------
@@ -116,13 +116,13 @@ class TestClientHeaderValue:
116
116
 
117
117
  def test_multi_hop_upstream_chain_is_preserved_in_order(self):
118
118
  value = client_header_value(
119
- "client=claude_web, client=mcp;ver=0.9",
119
+ "client=claude-web, client=mcp;ver=0.9",
120
120
  hop="client=python-sdk;ver=0.3.0",
121
121
  )
122
- assert value == "client=claude_web, client=mcp;ver=0.9, client=python-sdk;ver=0.3.0"
122
+ assert value == "client=claude-web, client=mcp;ver=0.9, client=python-sdk;ver=0.3.0"
123
123
  # Leftmost stays the origin, rightmost is this SDK.
124
124
  hops = [hop.strip() for hop in value.split(",")]
125
- assert hops[0] == "client=claude_web"
125
+ assert hops[0] == "client=claude-web"
126
126
  assert hops[-1] == "client=python-sdk;ver=0.3.0"
127
127
 
128
128
  def test_upstream_whitespace_is_normalized(self):