wherobots-python-sdk 0.4.0__tar.gz → 0.5.1__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.
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/PKG-INFO +51 -4
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/README.md +48 -1
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/pyproject.toml +14 -14
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_base_client.py +102 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_client_header.py +3 -3
- wherobots_python_sdk-0.5.1/tests/test_files_drive.py +811 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_init_exports.py +18 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/__init__.py +16 -1
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/__version__.py +1 -1
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/api/__init__.py +2 -1
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/api/base.py +81 -24
- wherobots_python_sdk-0.5.1/wherobots/api/drive.py +481 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/enums.py +10 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/exceptions.py +34 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/models.py +130 -0
- wherobots_python_sdk-0.5.1/wherobots/sdk.py +91 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots_python_sdk.egg-info/PKG-INFO +51 -4
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots_python_sdk.egg-info/SOURCES.txt +3 -0
- wherobots_python_sdk-0.5.1/wherobots_python_sdk.egg-info/requires.txt +2 -0
- wherobots_python_sdk-0.4.0/wherobots_python_sdk.egg-info/requires.txt +0 -2
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/LICENSE +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/setup.cfg +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_api.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_client.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_config.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_enums.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_exceptions.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_files_api.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_integration.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_logger.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_models.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_regressions.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_security.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/tests/test_utils.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/api/files.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/api/runs.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/client.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/config.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/py.typed +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/utils/__init__.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/utils/client_header.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/utils/logger.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots/utils/validation.py +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/wherobots_python_sdk.egg-info/dependency_links.txt +0 -0
- {wherobots_python_sdk-0.4.0 → wherobots_python_sdk-0.5.1}/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.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -21,8 +21,8 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: requests>=2.
|
|
25
|
-
Requires-Dist: boto3>=1.
|
|
24
|
+
Requires-Dist: requests>=2.34.2
|
|
25
|
+
Requires-Dist: boto3>=1.43.108
|
|
26
26
|
Dynamic: license-file
|
|
27
27
|
|
|
28
28
|
# Wherobots Python SDK
|
|
@@ -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` |
|
|
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` |
|
|
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>=
|
|
2
|
+
requires = ["setuptools>=83.0.0", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -25,8 +25,8 @@ classifiers = [
|
|
|
25
25
|
]
|
|
26
26
|
keywords = ["wherobots", "spark", "sedona", "geospatial", "jobs", "sdk", "rest-api"]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"requests>=2.
|
|
29
|
-
"boto3>=1.
|
|
28
|
+
"requests>=2.34.2",
|
|
29
|
+
"boto3>=1.43.108",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
@@ -38,17 +38,17 @@ Documentation = "https://docs.wherobots.com"
|
|
|
38
38
|
# PEP 735 dev dependencies — installed with `uv sync --group dev`.
|
|
39
39
|
[dependency-groups]
|
|
40
40
|
dev = [
|
|
41
|
-
"pytest>=
|
|
42
|
-
"pytest-cov>=
|
|
43
|
-
"pytest-mock>=3.
|
|
44
|
-
"ruff>=0.
|
|
45
|
-
"pyrefly>=
|
|
46
|
-
"import-linter>=2.
|
|
47
|
-
"pre-commit>=
|
|
48
|
-
"hypothesis>=6.
|
|
49
|
-
"cosmic-ray>=8.
|
|
50
|
-
"ast-grep-cli>=0.
|
|
51
|
-
"types-requests>=2.
|
|
41
|
+
"pytest>=9.1.1",
|
|
42
|
+
"pytest-cov>=7.1.0",
|
|
43
|
+
"pytest-mock>=3.16.0",
|
|
44
|
+
"ruff>=0.16.10,<0.17",
|
|
45
|
+
"pyrefly>=1.3.2",
|
|
46
|
+
"import-linter>=2.15",
|
|
47
|
+
"pre-commit>=4.6.2",
|
|
48
|
+
"hypothesis>=6.168.4",
|
|
49
|
+
"cosmic-ray>=8.7.0",
|
|
50
|
+
"ast-grep-cli>=0.45.3",
|
|
51
|
+
"types-requests>=2.33.0.20260906",
|
|
52
52
|
]
|
|
53
53
|
|
|
54
54
|
[tool.setuptools.packages.find]
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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):
|