wherobots-python-sdk 0.5.1__tar.gz → 0.7.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.
- {wherobots_python_sdk-0.5.1/wherobots_python_sdk.egg-info → wherobots_python_sdk-0.7.0}/PKG-INFO +66 -8
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/README.md +63 -4
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/pyproject.toml +18 -17
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_base_client.py +57 -4
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_config.py +12 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_files_api.py +16 -3
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_models.py +4 -4
- wherobots_python_sdk-0.7.0/tests/test_tasks.py +296 -0
- wherobots_python_sdk-0.7.0/tests/test_workload_auth.py +151 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/__init__.py +23 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/__version__.py +1 -1
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/__init__.py +2 -1
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/base.py +30 -7
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/drive.py +4 -18
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/files.py +8 -1
- wherobots_python_sdk-0.7.0/wherobots/api/tasks.py +265 -0
- wherobots_python_sdk-0.7.0/wherobots/api/workload_auth.py +116 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/config.py +12 -2
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/enums.py +33 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/exceptions.py +1 -1
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/models.py +347 -10
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/sdk.py +18 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0/wherobots_python_sdk.egg-info}/PKG-INFO +66 -8
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots_python_sdk.egg-info/SOURCES.txt +4 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots_python_sdk.egg-info/requires.txt +1 -1
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/LICENSE +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/setup.cfg +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_api.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_client.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_client_header.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_enums.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_exceptions.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_files_drive.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_init_exports.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_integration.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_logger.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_regressions.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_security.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_utils.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/runs.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/client.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/py.typed +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/utils/__init__.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/utils/client_header.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/utils/logger.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/utils/validation.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots_python_sdk.egg-info/dependency_links.txt +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots_python_sdk.egg-info/top_level.txt +0 -0
{wherobots_python_sdk-0.5.1/wherobots_python_sdk.egg-info → wherobots_python_sdk-0.7.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wherobots-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -13,22 +13,21 @@ Classifier: Development Status :: 4 - Beta
|
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.11
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
23
22
|
License-File: LICENSE
|
|
24
23
|
Requires-Dist: requests>=2.34.2
|
|
25
|
-
Requires-Dist: boto3>=1.
|
|
24
|
+
Requires-Dist: boto3>=1.34.0
|
|
26
25
|
Dynamic: license-file
|
|
27
26
|
|
|
28
27
|
# Wherobots Python SDK
|
|
29
28
|
|
|
30
29
|
[](https://github.com/wherobots/wherobots-python-sdk/actions/workflows/ci.yml)
|
|
31
|
-
[](https://www.python.org/downloads/)
|
|
32
31
|
[](LICENSE)
|
|
33
32
|
[](https://pypi.org/project/wherobots-python-sdk/)
|
|
34
33
|
|
|
@@ -46,6 +45,7 @@ Additional Wherobots surfaces will be added over time.
|
|
|
46
45
|
- Typed dataclass models for all API responses (no pydantic dependency)
|
|
47
46
|
- Environment-based configuration
|
|
48
47
|
- Files: list, create folders, upload, download, rename and delete in your Wherobots file area
|
|
48
|
+
- Tasks: list and run tasks such as RasterFlow inference, then follow their runs, logs and cost
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
@@ -118,18 +118,27 @@ job = WherobotsJob(
|
|
|
118
118
|
|
|
119
119
|
The API key can also be set via the `WHEROBOTS_API_KEY` environment variable.
|
|
120
120
|
|
|
121
|
+
Inside a Wherobots notebook or job, no API key is needed. The SDK authenticates as the user who
|
|
122
|
+
launched the workload, using the token Wherobots mounts in the pod. An API key, when set, takes
|
|
123
|
+
precedence.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
job = WherobotsJob(script="s3://bucket/script.py", name="my-job-001")
|
|
127
|
+
```
|
|
128
|
+
|
|
121
129
|
## Configuration
|
|
122
130
|
|
|
123
131
|
### Environment Variables
|
|
124
132
|
|
|
125
133
|
| Variable | Description | Default |
|
|
126
134
|
|------------------------------------|---------------------------------|--------------------------------------|
|
|
127
|
-
| `WHEROBOTS_API_KEY` | API key | *(required)*
|
|
135
|
+
| `WHEROBOTS_API_KEY` | API key | *(required outside Wherobots)* |
|
|
128
136
|
| `WHEROBOTS_REGION` | Region override (else org default) | *(none — org default)* |
|
|
129
137
|
| `WHEROBOTS_API_BASE_URL` | API base URL | `https://api.cloud.wherobots.com` |
|
|
130
138
|
| `WHEROBOTS_VERSION` | Wherobots version | `latest` |
|
|
131
139
|
| `WHEROBOTS_REQUEST_TIMEOUT_SECONDS`| HTTP request timeout (seconds) | `30` |
|
|
132
140
|
| `WHEROBOTS_CLIENT_CHAIN` | Upstream client-attribution chain (see below) | *(none)* |
|
|
141
|
+
| `WHEROBOTS_ID_TOKEN_PATH` | Workload identity token, set by Wherobots notebooks and jobs | *(none)* |
|
|
133
142
|
|
|
134
143
|
### WherobotsConfig
|
|
135
144
|
|
|
@@ -456,12 +465,53 @@ with wherobots.Wherobots() as wb:
|
|
|
456
465
|
| Failure | Raised as |
|
|
457
466
|
|---|---|
|
|
458
467
|
| Files is not enabled for this drive in this region | `WherobotsFilesNotEnabledError` (`.drive`, `.region`) |
|
|
459
|
-
| The
|
|
468
|
+
| The credentials were not accepted | `WherobotsNotAuthenticatedError` |
|
|
460
469
|
| No such file or folder | `WherobotsFileNotFoundError` |
|
|
461
470
|
| Transfer to or from storage failed | `WherobotsS3Error` |
|
|
462
471
|
|
|
463
472
|
The three `Wherobots*Error` classes above subclass `WherobotsAPIError`.
|
|
464
473
|
|
|
474
|
+
## Tasks
|
|
475
|
+
|
|
476
|
+
A task is a named unit of work, such as RasterFlow inference, that your organization can run.
|
|
477
|
+
`wb.tasks` lists and runs tasks; `wb.task_runs` follows the runs. Tasks need the Runs feature on
|
|
478
|
+
your organization.
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
import time
|
|
482
|
+
|
|
483
|
+
import wherobots
|
|
484
|
+
|
|
485
|
+
with wherobots.Wherobots() as wb:
|
|
486
|
+
for task in wb.tasks.list().items:
|
|
487
|
+
print(task.name, task.display_name, task.regions)
|
|
488
|
+
|
|
489
|
+
task = wb.tasks.get("my-task")
|
|
490
|
+
print(task.input_schema) # JSON Schema the inputs must satisfy
|
|
491
|
+
|
|
492
|
+
started = wb.tasks.run("my-task", {"aoi": "s3://bucket/aoi.geojson"}, name="nightly")
|
|
493
|
+
run = wb.task_runs.get(started.task_run_id)
|
|
494
|
+
while run.is_active:
|
|
495
|
+
time.sleep(10)
|
|
496
|
+
run = wb.task_runs.get(run.task_run_id)
|
|
497
|
+
|
|
498
|
+
print(run.state, run.results, run.error_message)
|
|
499
|
+
print(wb.task_runs.logs(run.task_run_id).entries)
|
|
500
|
+
print(wb.task_runs.cost(run.task_run_id).total_cost_usd)
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
- `tasks.run` uses `region=` if passed, then the configured region, then your organization's
|
|
504
|
+
default region.
|
|
505
|
+
- Listings return an `OffsetPage`; pass `page.next_offset` as `offset=` for the next one.
|
|
506
|
+
- `TaskRunState.is_active` is `True` for `PENDING`, `SUBMITTED`, `RUNNING` and `CANCELLING`. A
|
|
507
|
+
state the SDK does not know yet is kept as its raw string and counts as not active, so a
|
|
508
|
+
polling loop stops on it.
|
|
509
|
+
- A run's state can lag the run by up to 2 minutes.
|
|
510
|
+
- `task_runs.logs` pages forward: pass the previous page's `next_since` as `since=`. It is `None`
|
|
511
|
+
once the run has finished and every line has been returned.
|
|
512
|
+
- `task_runs.cost` can take up to 45 minutes after a run ends to settle; `is_settling` stays
|
|
513
|
+
`True` until then.
|
|
514
|
+
|
|
465
515
|
## Enums
|
|
466
516
|
|
|
467
517
|
### JobStatus
|
|
@@ -632,7 +682,7 @@ All exceptions inherit from `WherobotsJobError`.
|
|
|
632
682
|
| `WherobotsS3Error` | A transfer to or from storage failed, including Files presigned uploads and downloads. |
|
|
633
683
|
| `WherobotsConfigError` | Configuration is missing or invalid. |
|
|
634
684
|
| `WherobotsTimeoutError` | Job exceeded the specified timeout. |
|
|
635
|
-
| `WherobotsNotAuthenticatedError` | Files: the
|
|
685
|
+
| `WherobotsNotAuthenticatedError` | Files: the credentials were not accepted (HTTP 401). In a Wherobots pod: the workload identity token exchange failed. Subclass of `WherobotsAPIError`. |
|
|
636
686
|
| `WherobotsFilesNotEnabledError` | Files is not enabled for a drive in a region. Has `drive`, `region`. Subclass of `WherobotsAPIError`. |
|
|
637
687
|
| `WherobotsFileNotFoundError` | Files: no such file or folder. Subclass of `WherobotsAPIError`. |
|
|
638
688
|
|
|
@@ -689,6 +739,14 @@ To set up a Storage Integration, follow the
|
|
|
689
739
|
| GET | `/runs/{id}/logs` | Get run logs |
|
|
690
740
|
| GET | `/runs/{id}/metrics` | Get run metrics |
|
|
691
741
|
| GET | `/runs` | List runs |
|
|
742
|
+
| GET | `/tasks` | List tasks |
|
|
743
|
+
| GET | `/tasks/{name}` | Get a task |
|
|
744
|
+
| POST | `/tasks/{name}/runs` | Run a task |
|
|
745
|
+
| GET | `/task-runs` | List task runs |
|
|
746
|
+
| GET | `/task-runs/{id}` | Get a task run |
|
|
747
|
+
| GET | `/task-runs/{id}/logs` | Get task run logs |
|
|
748
|
+
| GET | `/task-runs/{id}/cost` | Get task run cost |
|
|
749
|
+
| POST | `/task-runs/{id}/cancel` | Cancel a task run |
|
|
692
750
|
|
|
693
751
|
Base URL: `https://api.cloud.wherobots.com`
|
|
694
752
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Wherobots Python SDK
|
|
2
2
|
|
|
3
3
|
[](https://github.com/wherobots/wherobots-python-sdk/actions/workflows/ci.yml)
|
|
4
|
-
[](https://www.python.org/downloads/)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://pypi.org/project/wherobots-python-sdk/)
|
|
7
7
|
|
|
@@ -19,6 +19,7 @@ Additional Wherobots surfaces will be added over time.
|
|
|
19
19
|
- Typed dataclass models for all API responses (no pydantic dependency)
|
|
20
20
|
- Environment-based configuration
|
|
21
21
|
- Files: list, create folders, upload, download, rename and delete in your Wherobots file area
|
|
22
|
+
- Tasks: list and run tasks such as RasterFlow inference, then follow their runs, logs and cost
|
|
22
23
|
|
|
23
24
|
## Installation
|
|
24
25
|
|
|
@@ -91,18 +92,27 @@ job = WherobotsJob(
|
|
|
91
92
|
|
|
92
93
|
The API key can also be set via the `WHEROBOTS_API_KEY` environment variable.
|
|
93
94
|
|
|
95
|
+
Inside a Wherobots notebook or job, no API key is needed. The SDK authenticates as the user who
|
|
96
|
+
launched the workload, using the token Wherobots mounts in the pod. An API key, when set, takes
|
|
97
|
+
precedence.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
job = WherobotsJob(script="s3://bucket/script.py", name="my-job-001")
|
|
101
|
+
```
|
|
102
|
+
|
|
94
103
|
## Configuration
|
|
95
104
|
|
|
96
105
|
### Environment Variables
|
|
97
106
|
|
|
98
107
|
| Variable | Description | Default |
|
|
99
108
|
|------------------------------------|---------------------------------|--------------------------------------|
|
|
100
|
-
| `WHEROBOTS_API_KEY` | API key | *(required)*
|
|
109
|
+
| `WHEROBOTS_API_KEY` | API key | *(required outside Wherobots)* |
|
|
101
110
|
| `WHEROBOTS_REGION` | Region override (else org default) | *(none — org default)* |
|
|
102
111
|
| `WHEROBOTS_API_BASE_URL` | API base URL | `https://api.cloud.wherobots.com` |
|
|
103
112
|
| `WHEROBOTS_VERSION` | Wherobots version | `latest` |
|
|
104
113
|
| `WHEROBOTS_REQUEST_TIMEOUT_SECONDS`| HTTP request timeout (seconds) | `30` |
|
|
105
114
|
| `WHEROBOTS_CLIENT_CHAIN` | Upstream client-attribution chain (see below) | *(none)* |
|
|
115
|
+
| `WHEROBOTS_ID_TOKEN_PATH` | Workload identity token, set by Wherobots notebooks and jobs | *(none)* |
|
|
106
116
|
|
|
107
117
|
### WherobotsConfig
|
|
108
118
|
|
|
@@ -429,12 +439,53 @@ with wherobots.Wherobots() as wb:
|
|
|
429
439
|
| Failure | Raised as |
|
|
430
440
|
|---|---|
|
|
431
441
|
| Files is not enabled for this drive in this region | `WherobotsFilesNotEnabledError` (`.drive`, `.region`) |
|
|
432
|
-
| The
|
|
442
|
+
| The credentials were not accepted | `WherobotsNotAuthenticatedError` |
|
|
433
443
|
| No such file or folder | `WherobotsFileNotFoundError` |
|
|
434
444
|
| Transfer to or from storage failed | `WherobotsS3Error` |
|
|
435
445
|
|
|
436
446
|
The three `Wherobots*Error` classes above subclass `WherobotsAPIError`.
|
|
437
447
|
|
|
448
|
+
## Tasks
|
|
449
|
+
|
|
450
|
+
A task is a named unit of work, such as RasterFlow inference, that your organization can run.
|
|
451
|
+
`wb.tasks` lists and runs tasks; `wb.task_runs` follows the runs. Tasks need the Runs feature on
|
|
452
|
+
your organization.
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
import time
|
|
456
|
+
|
|
457
|
+
import wherobots
|
|
458
|
+
|
|
459
|
+
with wherobots.Wherobots() as wb:
|
|
460
|
+
for task in wb.tasks.list().items:
|
|
461
|
+
print(task.name, task.display_name, task.regions)
|
|
462
|
+
|
|
463
|
+
task = wb.tasks.get("my-task")
|
|
464
|
+
print(task.input_schema) # JSON Schema the inputs must satisfy
|
|
465
|
+
|
|
466
|
+
started = wb.tasks.run("my-task", {"aoi": "s3://bucket/aoi.geojson"}, name="nightly")
|
|
467
|
+
run = wb.task_runs.get(started.task_run_id)
|
|
468
|
+
while run.is_active:
|
|
469
|
+
time.sleep(10)
|
|
470
|
+
run = wb.task_runs.get(run.task_run_id)
|
|
471
|
+
|
|
472
|
+
print(run.state, run.results, run.error_message)
|
|
473
|
+
print(wb.task_runs.logs(run.task_run_id).entries)
|
|
474
|
+
print(wb.task_runs.cost(run.task_run_id).total_cost_usd)
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
- `tasks.run` uses `region=` if passed, then the configured region, then your organization's
|
|
478
|
+
default region.
|
|
479
|
+
- Listings return an `OffsetPage`; pass `page.next_offset` as `offset=` for the next one.
|
|
480
|
+
- `TaskRunState.is_active` is `True` for `PENDING`, `SUBMITTED`, `RUNNING` and `CANCELLING`. A
|
|
481
|
+
state the SDK does not know yet is kept as its raw string and counts as not active, so a
|
|
482
|
+
polling loop stops on it.
|
|
483
|
+
- A run's state can lag the run by up to 2 minutes.
|
|
484
|
+
- `task_runs.logs` pages forward: pass the previous page's `next_since` as `since=`. It is `None`
|
|
485
|
+
once the run has finished and every line has been returned.
|
|
486
|
+
- `task_runs.cost` can take up to 45 minutes after a run ends to settle; `is_settling` stays
|
|
487
|
+
`True` until then.
|
|
488
|
+
|
|
438
489
|
## Enums
|
|
439
490
|
|
|
440
491
|
### JobStatus
|
|
@@ -605,7 +656,7 @@ All exceptions inherit from `WherobotsJobError`.
|
|
|
605
656
|
| `WherobotsS3Error` | A transfer to or from storage failed, including Files presigned uploads and downloads. |
|
|
606
657
|
| `WherobotsConfigError` | Configuration is missing or invalid. |
|
|
607
658
|
| `WherobotsTimeoutError` | Job exceeded the specified timeout. |
|
|
608
|
-
| `WherobotsNotAuthenticatedError` | Files: the
|
|
659
|
+
| `WherobotsNotAuthenticatedError` | Files: the credentials were not accepted (HTTP 401). In a Wherobots pod: the workload identity token exchange failed. Subclass of `WherobotsAPIError`. |
|
|
609
660
|
| `WherobotsFilesNotEnabledError` | Files is not enabled for a drive in a region. Has `drive`, `region`. Subclass of `WherobotsAPIError`. |
|
|
610
661
|
| `WherobotsFileNotFoundError` | Files: no such file or folder. Subclass of `WherobotsAPIError`. |
|
|
611
662
|
|
|
@@ -662,6 +713,14 @@ To set up a Storage Integration, follow the
|
|
|
662
713
|
| GET | `/runs/{id}/logs` | Get run logs |
|
|
663
714
|
| GET | `/runs/{id}/metrics` | Get run metrics |
|
|
664
715
|
| GET | `/runs` | List runs |
|
|
716
|
+
| GET | `/tasks` | List tasks |
|
|
717
|
+
| GET | `/tasks/{name}` | Get a task |
|
|
718
|
+
| POST | `/tasks/{name}/runs` | Run a task |
|
|
719
|
+
| GET | `/task-runs` | List task runs |
|
|
720
|
+
| GET | `/task-runs/{id}` | Get a task run |
|
|
721
|
+
| GET | `/task-runs/{id}/logs` | Get task run logs |
|
|
722
|
+
| GET | `/task-runs/{id}/cost` | Get task run cost |
|
|
723
|
+
| POST | `/task-runs/{id}/cancel` | Cancel a task run |
|
|
665
724
|
|
|
666
725
|
Base URL: `https://api.cloud.wherobots.com`
|
|
667
726
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=84.2.1", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -7,7 +7,7 @@ name = "wherobots-python-sdk"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "Python SDK for Wherobots (currently covers the Jobs REST API)"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Wherobots", email = "support@wherobots.com"}
|
|
@@ -17,7 +17,6 @@ classifiers = [
|
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
|
-
"Programming Language :: Python :: 3.10",
|
|
21
20
|
"Programming Language :: Python :: 3.11",
|
|
22
21
|
"Programming Language :: Python :: 3.12",
|
|
23
22
|
"Programming Language :: Python :: 3.13",
|
|
@@ -26,7 +25,7 @@ classifiers = [
|
|
|
26
25
|
keywords = ["wherobots", "spark", "sedona", "geospatial", "jobs", "sdk", "rest-api"]
|
|
27
26
|
dependencies = [
|
|
28
27
|
"requests>=2.34.2",
|
|
29
|
-
"boto3>=1.
|
|
28
|
+
"boto3>=1.34.0",
|
|
30
29
|
]
|
|
31
30
|
|
|
32
31
|
[project.urls]
|
|
@@ -38,17 +37,17 @@ Documentation = "https://docs.wherobots.com"
|
|
|
38
37
|
# PEP 735 dev dependencies — installed with `uv sync --group dev`.
|
|
39
38
|
[dependency-groups]
|
|
40
39
|
dev = [
|
|
41
|
-
"pytest
|
|
42
|
-
"pytest-cov
|
|
43
|
-
"pytest-mock
|
|
44
|
-
"ruff
|
|
45
|
-
"pyrefly
|
|
46
|
-
"import-linter
|
|
47
|
-
"pre-commit
|
|
48
|
-
"hypothesis
|
|
49
|
-
"cosmic-ray
|
|
50
|
-
"ast-grep-cli
|
|
51
|
-
"types-requests
|
|
40
|
+
"pytest==9.1.1",
|
|
41
|
+
"pytest-cov==7.1.0",
|
|
42
|
+
"pytest-mock==3.17.0",
|
|
43
|
+
"ruff==0.17.3",
|
|
44
|
+
"pyrefly==1.4.0",
|
|
45
|
+
"import-linter==2.15",
|
|
46
|
+
"pre-commit==4.7.0",
|
|
47
|
+
"hypothesis==6.170.12",
|
|
48
|
+
"cosmic-ray==8.7.0",
|
|
49
|
+
"ast-grep-cli==0.51.0",
|
|
50
|
+
"types-requests==2.33.0.20261006",
|
|
52
51
|
]
|
|
53
52
|
|
|
54
53
|
[tool.setuptools.packages.find]
|
|
@@ -60,12 +59,14 @@ wherobots = ["py.typed"]
|
|
|
60
59
|
[tool.setuptools.dynamic]
|
|
61
60
|
version = {attr = "wherobots.__version__.__version__"}
|
|
62
61
|
|
|
63
|
-
# SLOP REPELLANT ruff rail: line-length 100,
|
|
64
|
-
#
|
|
62
|
+
# SLOP REPELLANT ruff rail: line-length 100, select = ["ALL"] and the
|
|
63
|
+
# typing.Any/typing.cast ban come from the base config. The base targets
|
|
64
|
+
# py310 (the kit's floor); this project overrides it to its own py311 floor.
|
|
65
65
|
# NOTE: `lint.select`/`lint.ignore` would REPLACE the base's — tune only via
|
|
66
66
|
# the extend-* keys.
|
|
67
67
|
[tool.ruff]
|
|
68
68
|
extend = ".slop-repellant/ruff-base.toml"
|
|
69
|
+
target-version = "py311"
|
|
69
70
|
# .slop-repellant/ is vendored, kit-MANAGED tooling (re-rendered on
|
|
70
71
|
# slop-repellant-update) — linting it locally would fork the managed files.
|
|
71
72
|
extend-exclude = ["env", ".venv", "venv", "build", "dist", ".slop-repellant"]
|
|
@@ -158,17 +158,70 @@ class TestClientAttributionHeader:
|
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
class TestClientConfigValidation:
|
|
161
|
-
def
|
|
161
|
+
def test_missing_credentials_raises(self):
|
|
162
162
|
cfg = WherobotsConfig(api_key=None)
|
|
163
|
-
with pytest.raises(WherobotsConfigError, match="
|
|
163
|
+
with pytest.raises(WherobotsConfigError, match="No Wherobots credentials found"):
|
|
164
164
|
BaseClient(cfg)
|
|
165
165
|
|
|
166
166
|
def test_empty_api_key_raises(self):
|
|
167
|
-
cfg = WherobotsConfig(api_key="")
|
|
168
|
-
with pytest.raises(WherobotsConfigError, match="
|
|
167
|
+
cfg = WherobotsConfig(api_key=" ")
|
|
168
|
+
with pytest.raises(WherobotsConfigError, match="No Wherobots credentials found"):
|
|
169
169
|
BaseClient(cfg)
|
|
170
170
|
|
|
171
171
|
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# Credential selection
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestCredentialSelection:
|
|
178
|
+
@patch("wherobots.api.base.requests.Session")
|
|
179
|
+
def test_api_key_wins_over_workload_identity(self, MockSession):
|
|
180
|
+
session = MockSession.return_value
|
|
181
|
+
session.headers = {}
|
|
182
|
+
session.auth = None
|
|
183
|
+
|
|
184
|
+
BaseClient(
|
|
185
|
+
WherobotsConfig(
|
|
186
|
+
api_key="test-key",
|
|
187
|
+
id_token_path="/var/run/secrets/wherobots/id-token",
|
|
188
|
+
base_url="https://api.test.wherobots.com",
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert session.headers["X-API-Key"] == "test-key"
|
|
193
|
+
assert session.auth is None
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize("api_key", [None, " "])
|
|
196
|
+
@patch("wherobots.api.workload_auth.requests.post")
|
|
197
|
+
@patch("requests.adapters.HTTPAdapter.send")
|
|
198
|
+
def test_workload_identity_without_api_key(self, mock_send, mock_post, api_key, tmp_path):
|
|
199
|
+
id_token = tmp_path / "id-token"
|
|
200
|
+
id_token.write_text("wid-token", encoding="utf-8")
|
|
201
|
+
exchange = MagicMock(status_code=200)
|
|
202
|
+
exchange.json.return_value = {"access_token": "access-1", "expires_in": 3600}
|
|
203
|
+
mock_post.return_value = exchange
|
|
204
|
+
api_response = requests.Response()
|
|
205
|
+
api_response.status_code = 200
|
|
206
|
+
mock_send.return_value = api_response
|
|
207
|
+
|
|
208
|
+
client = BaseClient(
|
|
209
|
+
WherobotsConfig(
|
|
210
|
+
api_key=api_key,
|
|
211
|
+
id_token_path=str(id_token),
|
|
212
|
+
base_url="https://api.test.wherobots.com/",
|
|
213
|
+
request_timeout_seconds=12,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
client.get("/runs")
|
|
217
|
+
|
|
218
|
+
sent = mock_send.call_args.args[0]
|
|
219
|
+
assert sent.headers["Authorization"] == "Bearer access-1"
|
|
220
|
+
assert "X-API-Key" not in sent.headers
|
|
221
|
+
assert mock_post.call_args.args == ("https://api.test.wherobots.com/ste/oauth2/token",)
|
|
222
|
+
assert mock_post.call_args.kwargs["timeout"] == 12
|
|
223
|
+
|
|
224
|
+
|
|
172
225
|
# ---------------------------------------------------------------------------
|
|
173
226
|
# Request error wrapping
|
|
174
227
|
# ---------------------------------------------------------------------------
|
|
@@ -88,6 +88,7 @@ class TestFromEnv:
|
|
|
88
88
|
"WHEROBOTS_VERSION",
|
|
89
89
|
"WHEROBOTS_REQUEST_TIMEOUT_SECONDS",
|
|
90
90
|
"WHEROBOTS_CLIENT_CHAIN",
|
|
91
|
+
"WHEROBOTS_ID_TOKEN_PATH",
|
|
91
92
|
]:
|
|
92
93
|
monkeypatch.delenv(var, raising=False)
|
|
93
94
|
|
|
@@ -100,6 +101,7 @@ class TestFromEnv:
|
|
|
100
101
|
assert cfg.version == "latest"
|
|
101
102
|
assert cfg.request_timeout_seconds == 30
|
|
102
103
|
assert cfg.client_chain is None
|
|
104
|
+
assert cfg.id_token_path is None
|
|
103
105
|
|
|
104
106
|
def test_from_env_timeout_override(self, monkeypatch):
|
|
105
107
|
monkeypatch.delenv("WHEROBOTS_REQUEST_TIMEOUT_SECONDS", raising=False)
|
|
@@ -116,6 +118,16 @@ class TestFromEnv:
|
|
|
116
118
|
cfg = WherobotsConfig.from_env(client_chain="client=mcp;ver=0.9")
|
|
117
119
|
assert cfg.client_chain == "client=mcp;ver=0.9"
|
|
118
120
|
|
|
121
|
+
def test_blank_explicit_api_key_falls_back_to_env(self, monkeypatch):
|
|
122
|
+
monkeypatch.setenv("WHEROBOTS_API_KEY", "env-key")
|
|
123
|
+
cfg = WherobotsConfig.from_env(api_key=" ")
|
|
124
|
+
assert cfg.api_key == "env-key"
|
|
125
|
+
|
|
126
|
+
def test_id_token_path_from_env(self, monkeypatch):
|
|
127
|
+
monkeypatch.setenv("WHEROBOTS_ID_TOKEN_PATH", "/var/run/secrets/wherobots/id-token")
|
|
128
|
+
cfg = WherobotsConfig.from_env()
|
|
129
|
+
assert cfg.id_token_path == "/var/run/secrets/wherobots/id-token"
|
|
130
|
+
|
|
119
131
|
|
|
120
132
|
# ---------------------------------------------------------------------------
|
|
121
133
|
# Frozen
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from datetime import datetime, timedelta
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
7
|
from unittest.mock import MagicMock, patch
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
@@ -14,7 +14,11 @@ from wherobots.api.files import (
|
|
|
14
14
|
_validate_filename,
|
|
15
15
|
)
|
|
16
16
|
from wherobots.config import WherobotsConfig
|
|
17
|
-
from wherobots.exceptions import
|
|
17
|
+
from wherobots.exceptions import (
|
|
18
|
+
WherobotsAPIError,
|
|
19
|
+
WherobotsNotAuthenticatedError,
|
|
20
|
+
WherobotsValidationError,
|
|
21
|
+
)
|
|
18
22
|
from wherobots.models import StorageIntegration, TemporaryAwsCredentials
|
|
19
23
|
|
|
20
24
|
# ---------------------------------------------------------------------------
|
|
@@ -45,7 +49,7 @@ def files_api(mock_env):
|
|
|
45
49
|
@pytest.fixture
|
|
46
50
|
def creds_payload():
|
|
47
51
|
"""Sample TemporaryAwsCredentials JSON, well in the future."""
|
|
48
|
-
expiry = (datetime.now(
|
|
52
|
+
expiry = (datetime.now(UTC) + timedelta(hours=1)).isoformat()
|
|
49
53
|
return {
|
|
50
54
|
"accessKeyId": "ASIAEXAMPLE0001",
|
|
51
55
|
"secretAccessKey": "supersecretvaluedoNotleak",
|
|
@@ -110,6 +114,15 @@ class TestGetStorageCredentials:
|
|
|
110
114
|
with pytest.raises(WherobotsAPIError, match="Invalid JSON"):
|
|
111
115
|
api.get_storage_credentials("s3://x/y")
|
|
112
116
|
|
|
117
|
+
def test_workload_token_exchange_failure_passes_through(self, files_api):
|
|
118
|
+
api, session = files_api
|
|
119
|
+
session.request.side_effect = WherobotsNotAuthenticatedError(
|
|
120
|
+
"Workload identity token exchange was rejected (status 401): Session is expired"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
with pytest.raises(WherobotsNotAuthenticatedError, match="Session is expired"):
|
|
124
|
+
api.get_storage_credentials("s3://x/y")
|
|
125
|
+
|
|
113
126
|
def test_default_access_mode_is_read(self, files_api, creds_payload):
|
|
114
127
|
"""Least-privilege default: public callers get read-only creds unless
|
|
115
128
|
they explicitly opt into read_write. The SDK's upload paths pass
|
|
@@ -987,7 +987,7 @@ class TestStorageIntegrationTypes:
|
|
|
987
987
|
# ---------------------------------------------------------------------------
|
|
988
988
|
|
|
989
989
|
|
|
990
|
-
from datetime import datetime, timedelta
|
|
990
|
+
from datetime import UTC, datetime, timedelta # noqa: E402
|
|
991
991
|
|
|
992
992
|
|
|
993
993
|
class TestTemporaryAwsCredentials:
|
|
@@ -1006,7 +1006,7 @@ class TestTemporaryAwsCredentials:
|
|
|
1006
1006
|
assert creds.expiration.year == 2026
|
|
1007
1007
|
|
|
1008
1008
|
def test_is_expired_true_for_past(self):
|
|
1009
|
-
past = datetime.now(
|
|
1009
|
+
past = datetime.now(UTC) - timedelta(seconds=1)
|
|
1010
1010
|
creds = TemporaryAwsCredentials(
|
|
1011
1011
|
access_key_id="ASIA1234",
|
|
1012
1012
|
secret_access_key="s",
|
|
@@ -1018,7 +1018,7 @@ class TestTemporaryAwsCredentials:
|
|
|
1018
1018
|
|
|
1019
1019
|
def test_is_expired_respects_leeway(self):
|
|
1020
1020
|
# 30s in the future, leeway 60s ⇒ considered expired.
|
|
1021
|
-
soon = datetime.now(
|
|
1021
|
+
soon = datetime.now(UTC) + timedelta(seconds=30)
|
|
1022
1022
|
creds = TemporaryAwsCredentials(
|
|
1023
1023
|
access_key_id="ASIA1234",
|
|
1024
1024
|
secret_access_key="s",
|
|
@@ -1034,7 +1034,7 @@ class TestTemporaryAwsCredentials:
|
|
|
1034
1034
|
access_key_id="ASIAEXAMPLE1234",
|
|
1035
1035
|
secret_access_key="ULTRASECRETzzzzzzzz",
|
|
1036
1036
|
session_token="TOKEN_payload_here",
|
|
1037
|
-
expiration=datetime(2026, 1, 1, tzinfo=
|
|
1037
|
+
expiration=datetime(2026, 1, 1, tzinfo=UTC),
|
|
1038
1038
|
region="us-west-2",
|
|
1039
1039
|
)
|
|
1040
1040
|
rendered = repr(creds)
|