wherobots-python-sdk 0.5.1__tar.gz → 0.6.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.6.0}/PKG-INFO +53 -4
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/README.md +51 -1
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/pyproject.toml +17 -16
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_files_api.py +2 -2
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_models.py +4 -4
- wherobots_python_sdk-0.6.0/tests/test_tasks.py +296 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/__init__.py +23 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/__version__.py +1 -1
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/api/__init__.py +2 -1
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/api/base.py +15 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/api/drive.py +3 -17
- wherobots_python_sdk-0.6.0/wherobots/api/tasks.py +265 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/enums.py +33 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/models.py +347 -10
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/sdk.py +18 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0/wherobots_python_sdk.egg-info}/PKG-INFO +53 -4
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots_python_sdk.egg-info/SOURCES.txt +2 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/LICENSE +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/setup.cfg +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_api.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_base_client.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_client.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_client_header.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_config.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_enums.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_exceptions.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_files_drive.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_init_exports.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_integration.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_logger.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_regressions.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_security.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/tests/test_utils.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/api/files.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/api/runs.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/client.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/config.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/exceptions.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/py.typed +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/utils/__init__.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/utils/client_header.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/utils/logger.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots/utils/validation.py +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots_python_sdk.egg-info/dependency_links.txt +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.0}/wherobots_python_sdk.egg-info/requires.txt +0 -0
- {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.6.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.6.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.6.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,12 +13,11 @@ 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
|
|
@@ -28,7 +27,7 @@ Dynamic: license-file
|
|
|
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
|
|
|
@@ -462,6 +462,47 @@ with wherobots.Wherobots() as wb:
|
|
|
462
462
|
|
|
463
463
|
The three `Wherobots*Error` classes above subclass `WherobotsAPIError`.
|
|
464
464
|
|
|
465
|
+
## Tasks
|
|
466
|
+
|
|
467
|
+
A task is a named unit of work, such as RasterFlow inference, that your organization can run.
|
|
468
|
+
`wb.tasks` lists and runs tasks; `wb.task_runs` follows the runs. Tasks need the Runs feature on
|
|
469
|
+
your organization.
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
import time
|
|
473
|
+
|
|
474
|
+
import wherobots
|
|
475
|
+
|
|
476
|
+
with wherobots.Wherobots() as wb:
|
|
477
|
+
for task in wb.tasks.list().items:
|
|
478
|
+
print(task.name, task.display_name, task.regions)
|
|
479
|
+
|
|
480
|
+
task = wb.tasks.get("my-task")
|
|
481
|
+
print(task.input_schema) # JSON Schema the inputs must satisfy
|
|
482
|
+
|
|
483
|
+
started = wb.tasks.run("my-task", {"aoi": "s3://bucket/aoi.geojson"}, name="nightly")
|
|
484
|
+
run = wb.task_runs.get(started.task_run_id)
|
|
485
|
+
while run.is_active:
|
|
486
|
+
time.sleep(10)
|
|
487
|
+
run = wb.task_runs.get(run.task_run_id)
|
|
488
|
+
|
|
489
|
+
print(run.state, run.results, run.error_message)
|
|
490
|
+
print(wb.task_runs.logs(run.task_run_id).entries)
|
|
491
|
+
print(wb.task_runs.cost(run.task_run_id).total_cost_usd)
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
- `tasks.run` uses `region=` if passed, then the configured region, then your organization's
|
|
495
|
+
default region.
|
|
496
|
+
- Listings return an `OffsetPage`; pass `page.next_offset` as `offset=` for the next one.
|
|
497
|
+
- `TaskRunState.is_active` is `True` for `PENDING`, `SUBMITTED`, `RUNNING` and `CANCELLING`. A
|
|
498
|
+
state the SDK does not know yet is kept as its raw string and counts as not active, so a
|
|
499
|
+
polling loop stops on it.
|
|
500
|
+
- A run's state can lag the run by up to 2 minutes.
|
|
501
|
+
- `task_runs.logs` pages forward: pass the previous page's `next_since` as `since=`. It is `None`
|
|
502
|
+
once the run has finished and every line has been returned.
|
|
503
|
+
- `task_runs.cost` can take up to 45 minutes after a run ends to settle; `is_settling` stays
|
|
504
|
+
`True` until then.
|
|
505
|
+
|
|
465
506
|
## Enums
|
|
466
507
|
|
|
467
508
|
### JobStatus
|
|
@@ -689,6 +730,14 @@ To set up a Storage Integration, follow the
|
|
|
689
730
|
| GET | `/runs/{id}/logs` | Get run logs |
|
|
690
731
|
| GET | `/runs/{id}/metrics` | Get run metrics |
|
|
691
732
|
| GET | `/runs` | List runs |
|
|
733
|
+
| GET | `/tasks` | List tasks |
|
|
734
|
+
| GET | `/tasks/{name}` | Get a task |
|
|
735
|
+
| POST | `/tasks/{name}/runs` | Run a task |
|
|
736
|
+
| GET | `/task-runs` | List task runs |
|
|
737
|
+
| GET | `/task-runs/{id}` | Get a task run |
|
|
738
|
+
| GET | `/task-runs/{id}/logs` | Get task run logs |
|
|
739
|
+
| GET | `/task-runs/{id}/cost` | Get task run cost |
|
|
740
|
+
| POST | `/task-runs/{id}/cancel` | Cancel a task run |
|
|
692
741
|
|
|
693
742
|
Base URL: `https://api.cloud.wherobots.com`
|
|
694
743
|
|
|
@@ -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
|
|
|
@@ -435,6 +436,47 @@ with wherobots.Wherobots() as wb:
|
|
|
435
436
|
|
|
436
437
|
The three `Wherobots*Error` classes above subclass `WherobotsAPIError`.
|
|
437
438
|
|
|
439
|
+
## Tasks
|
|
440
|
+
|
|
441
|
+
A task is a named unit of work, such as RasterFlow inference, that your organization can run.
|
|
442
|
+
`wb.tasks` lists and runs tasks; `wb.task_runs` follows the runs. Tasks need the Runs feature on
|
|
443
|
+
your organization.
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
import time
|
|
447
|
+
|
|
448
|
+
import wherobots
|
|
449
|
+
|
|
450
|
+
with wherobots.Wherobots() as wb:
|
|
451
|
+
for task in wb.tasks.list().items:
|
|
452
|
+
print(task.name, task.display_name, task.regions)
|
|
453
|
+
|
|
454
|
+
task = wb.tasks.get("my-task")
|
|
455
|
+
print(task.input_schema) # JSON Schema the inputs must satisfy
|
|
456
|
+
|
|
457
|
+
started = wb.tasks.run("my-task", {"aoi": "s3://bucket/aoi.geojson"}, name="nightly")
|
|
458
|
+
run = wb.task_runs.get(started.task_run_id)
|
|
459
|
+
while run.is_active:
|
|
460
|
+
time.sleep(10)
|
|
461
|
+
run = wb.task_runs.get(run.task_run_id)
|
|
462
|
+
|
|
463
|
+
print(run.state, run.results, run.error_message)
|
|
464
|
+
print(wb.task_runs.logs(run.task_run_id).entries)
|
|
465
|
+
print(wb.task_runs.cost(run.task_run_id).total_cost_usd)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
- `tasks.run` uses `region=` if passed, then the configured region, then your organization's
|
|
469
|
+
default region.
|
|
470
|
+
- Listings return an `OffsetPage`; pass `page.next_offset` as `offset=` for the next one.
|
|
471
|
+
- `TaskRunState.is_active` is `True` for `PENDING`, `SUBMITTED`, `RUNNING` and `CANCELLING`. A
|
|
472
|
+
state the SDK does not know yet is kept as its raw string and counts as not active, so a
|
|
473
|
+
polling loop stops on it.
|
|
474
|
+
- A run's state can lag the run by up to 2 minutes.
|
|
475
|
+
- `task_runs.logs` pages forward: pass the previous page's `next_since` as `since=`. It is `None`
|
|
476
|
+
once the run has finished and every line has been returned.
|
|
477
|
+
- `task_runs.cost` can take up to 45 minutes after a run ends to settle; `is_settling` stays
|
|
478
|
+
`True` until then.
|
|
479
|
+
|
|
438
480
|
## Enums
|
|
439
481
|
|
|
440
482
|
### JobStatus
|
|
@@ -662,6 +704,14 @@ To set up a Storage Integration, follow the
|
|
|
662
704
|
| GET | `/runs/{id}/logs` | Get run logs |
|
|
663
705
|
| GET | `/runs/{id}/metrics` | Get run metrics |
|
|
664
706
|
| GET | `/runs` | List runs |
|
|
707
|
+
| GET | `/tasks` | List tasks |
|
|
708
|
+
| GET | `/tasks/{name}` | Get a task |
|
|
709
|
+
| POST | `/tasks/{name}/runs` | Run a task |
|
|
710
|
+
| GET | `/task-runs` | List task runs |
|
|
711
|
+
| GET | `/task-runs/{id}` | Get a task run |
|
|
712
|
+
| GET | `/task-runs/{id}/logs` | Get task run logs |
|
|
713
|
+
| GET | `/task-runs/{id}/cost` | Get task run cost |
|
|
714
|
+
| POST | `/task-runs/{id}/cancel` | Cancel a task run |
|
|
665
715
|
|
|
666
716
|
Base URL: `https://api.cloud.wherobots.com`
|
|
667
717
|
|
|
@@ -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",
|
|
@@ -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.16.0",
|
|
43
|
+
"ruff==0.17.0",
|
|
44
|
+
"pyrefly==1.3.2",
|
|
45
|
+
"import-linter==2.15",
|
|
46
|
+
"pre-commit==4.7.0",
|
|
47
|
+
"hypothesis==6.170.2",
|
|
48
|
+
"cosmic-ray==8.7.0",
|
|
49
|
+
"ast-grep-cli==0.50.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"]
|
|
@@ -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
|
|
@@ -45,7 +45,7 @@ def files_api(mock_env):
|
|
|
45
45
|
@pytest.fixture
|
|
46
46
|
def creds_payload():
|
|
47
47
|
"""Sample TemporaryAwsCredentials JSON, well in the future."""
|
|
48
|
-
expiry = (datetime.now(
|
|
48
|
+
expiry = (datetime.now(UTC) + timedelta(hours=1)).isoformat()
|
|
49
49
|
return {
|
|
50
50
|
"accessKeyId": "ASIAEXAMPLE0001",
|
|
51
51
|
"secretAccessKey": "supersecretvaluedoNotleak",
|
|
@@ -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)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Tests for the Tasks surface: Wherobots -> Tasks / TaskRuns."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import requests
|
|
9
|
+
from hypothesis import given
|
|
10
|
+
from hypothesis import strategies as st
|
|
11
|
+
|
|
12
|
+
from wherobots import (
|
|
13
|
+
OffsetPage,
|
|
14
|
+
Region,
|
|
15
|
+
Task,
|
|
16
|
+
TaskRun,
|
|
17
|
+
TaskRunState,
|
|
18
|
+
TaskRunSummary,
|
|
19
|
+
Wherobots,
|
|
20
|
+
WherobotsAPIError,
|
|
21
|
+
WherobotsConfig,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
BASE = "https://api.test.wherobots.com"
|
|
25
|
+
|
|
26
|
+
TASK_RUN = {
|
|
27
|
+
"taskRunId": "tr_1",
|
|
28
|
+
"taskName": "rasterflow-inference",
|
|
29
|
+
"state": "SUCCEEDED",
|
|
30
|
+
"region": "aws-us-west-2",
|
|
31
|
+
"submittedAt": "2026-10-20T18:00:00Z",
|
|
32
|
+
"startedAt": "2026-10-20T18:00:05Z",
|
|
33
|
+
"endedAt": "2026-10-20T18:10:05Z",
|
|
34
|
+
"durationSeconds": 600,
|
|
35
|
+
"name": "nightly",
|
|
36
|
+
"inputs": {"aoi": "s3://bucket/aoi.geojson"},
|
|
37
|
+
"results": {"output": "s3://bucket/out.parquet"},
|
|
38
|
+
"cached": True,
|
|
39
|
+
"errorMessage": None,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def session():
|
|
45
|
+
with patch("wherobots.api.base.requests.Session") as mock_session_cls:
|
|
46
|
+
sess = mock_session_cls.return_value
|
|
47
|
+
sess.headers = {}
|
|
48
|
+
yield sess
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _wb(session, region="aws-us-west-2", body=None):
|
|
52
|
+
session.request.return_value = _ok(body)
|
|
53
|
+
return Wherobots(WherobotsConfig(api_key="test-key-123", region=region, base_url=BASE))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ok(body=None):
|
|
57
|
+
resp = MagicMock()
|
|
58
|
+
resp.status_code = 200
|
|
59
|
+
resp.json.return_value = body if body is not None else {}
|
|
60
|
+
resp.raise_for_status.return_value = None
|
|
61
|
+
return resp
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _sent(session):
|
|
65
|
+
call = session.request.call_args
|
|
66
|
+
return call.args[0], call.args[1], call.kwargs["params"], call.kwargs["json"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_surfaces_are_cached_on_the_client(session):
|
|
70
|
+
wb = _wb(session)
|
|
71
|
+
assert wb.tasks is wb.tasks
|
|
72
|
+
assert wb.task_runs is wb.task_runs
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_list_tasks_sends_filters_and_parses_the_page(session):
|
|
76
|
+
body = {
|
|
77
|
+
"items": [{"name": "a", "displayName": "A", "regions": ["aws-us-west-2", 7]}],
|
|
78
|
+
"total": 3,
|
|
79
|
+
"limit": 1,
|
|
80
|
+
"offset": 1,
|
|
81
|
+
}
|
|
82
|
+
wb = _wb(session, body=body)
|
|
83
|
+
|
|
84
|
+
page = wb.tasks.list(regions=[Region.US_WEST_2, "aws-eu-west-1"], limit=1, offset=1)
|
|
85
|
+
|
|
86
|
+
assert _sent(session) == (
|
|
87
|
+
"GET",
|
|
88
|
+
f"{BASE}/tasks",
|
|
89
|
+
{"limit": 1, "offset": 1, "regions": ["aws-us-west-2", "aws-eu-west-1"]},
|
|
90
|
+
None,
|
|
91
|
+
)
|
|
92
|
+
assert [t.name for t in page.items] == ["a"]
|
|
93
|
+
assert page.items[0].regions == ["aws-us-west-2"]
|
|
94
|
+
assert page.next_offset == 2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize("regions", ["aws-us-west-2", Region.US_WEST_2])
|
|
98
|
+
def test_one_region_is_not_split_into_characters(session, regions):
|
|
99
|
+
wb = _wb(session, body={"items": [], "total": 0, "limit": 50, "offset": 0})
|
|
100
|
+
wb.tasks.list(regions=regions)
|
|
101
|
+
assert _sent(session)[2]["regions"] == ["aws-us-west-2"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_get_task_encodes_the_name_and_parses_schemas(session):
|
|
105
|
+
body = {
|
|
106
|
+
"name": "a.b",
|
|
107
|
+
"displayName": "A",
|
|
108
|
+
"inputSchema": {"type": "object"},
|
|
109
|
+
"examples": [{"name": "small", "inputs": {"n": 1}}, "junk"],
|
|
110
|
+
}
|
|
111
|
+
wb = _wb(session, body=body)
|
|
112
|
+
|
|
113
|
+
task = wb.tasks.get("a/b")
|
|
114
|
+
|
|
115
|
+
assert _sent(session)[1] == f"{BASE}/tasks/a%2Fb"
|
|
116
|
+
assert isinstance(task, Task)
|
|
117
|
+
assert task.input_schema == {"type": "object"}
|
|
118
|
+
assert task.output_schema is None
|
|
119
|
+
assert [(e.name, e.inputs) for e in task.examples] == [("small", {"n": 1})]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize(
|
|
123
|
+
("configured", "explicit", "expected"),
|
|
124
|
+
[
|
|
125
|
+
("aws-us-west-2", None, "aws-us-west-2"),
|
|
126
|
+
("aws-us-west-2", Region.EU_WEST_1, "aws-eu-west-1"),
|
|
127
|
+
(None, None, None),
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
def test_run_task_picks_the_region(session, configured, explicit, expected):
|
|
131
|
+
wb = _wb(session, region=configured, body=TASK_RUN)
|
|
132
|
+
|
|
133
|
+
run = wb.tasks.run("rasterflow-inference", {"aoi": "x"}, region=explicit)
|
|
134
|
+
|
|
135
|
+
method, url, _, body = _sent(session)
|
|
136
|
+
assert (method, url) == ("POST", f"{BASE}/tasks/rasterflow-inference/runs")
|
|
137
|
+
assert body == {"inputs": {"aoi": "x"}} | ({"region": expected} if expected else {})
|
|
138
|
+
assert isinstance(run, TaskRunSummary)
|
|
139
|
+
assert run.task_run_id == "tr_1"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_run_task_sends_the_run_name(session):
|
|
143
|
+
wb = _wb(session, body=TASK_RUN)
|
|
144
|
+
wb.tasks.run("rasterflow-inference", name="nightly")
|
|
145
|
+
assert _sent(session)[3]["name"] == "nightly"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_list_task_runs_sends_filters(session):
|
|
149
|
+
wb = _wb(session, body={"items": [TASK_RUN], "total": 1, "limit": 50, "offset": 0})
|
|
150
|
+
after = datetime(2026, 10, 1, tzinfo=UTC)
|
|
151
|
+
|
|
152
|
+
page = wb.task_runs.list(
|
|
153
|
+
task_name="raster*", states=[TaskRunState.RUNNING, "PENDING"], created_after=after
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert _sent(session)[2] == {
|
|
157
|
+
"limit": 50,
|
|
158
|
+
"offset": 0,
|
|
159
|
+
"taskName": "raster*",
|
|
160
|
+
"state": ["RUNNING", "PENDING"],
|
|
161
|
+
"createdAfter": "2026-10-01T00:00:00+00:00",
|
|
162
|
+
}
|
|
163
|
+
assert page.items[0].state is TaskRunState.SUCCEEDED
|
|
164
|
+
assert page.next_offset is None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_list_task_runs_reads_a_naive_datetime_as_local_time(session):
|
|
168
|
+
wb = _wb(session, body={"items": [], "total": 0, "limit": 50, "offset": 0})
|
|
169
|
+
naive = datetime(2026, 10, 1, 9, 30)
|
|
170
|
+
|
|
171
|
+
wb.task_runs.list(created_after=naive, states="RUNNING")
|
|
172
|
+
|
|
173
|
+
params = _sent(session)[2]
|
|
174
|
+
assert datetime.fromisoformat(params["createdAfter"]) == naive.astimezone()
|
|
175
|
+
assert params["createdAfter"].endswith("+00:00")
|
|
176
|
+
assert params["state"] == ["RUNNING"]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_get_task_run_parses_the_outcome(session):
|
|
180
|
+
wb = _wb(session, body=TASK_RUN)
|
|
181
|
+
|
|
182
|
+
run = wb.task_runs.get("tr/1")
|
|
183
|
+
|
|
184
|
+
assert _sent(session)[1] == f"{BASE}/task-runs/tr%2F1"
|
|
185
|
+
assert isinstance(run, TaskRun)
|
|
186
|
+
assert run.results == {"output": "s3://bucket/out.parquet"}
|
|
187
|
+
assert run.cached is True
|
|
188
|
+
assert run.duration_seconds == 600
|
|
189
|
+
assert run.submitted_at == datetime(2026, 10, 20, 18, tzinfo=UTC)
|
|
190
|
+
assert not run.is_active
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_task_run_keeps_an_unknown_state():
|
|
194
|
+
run = TaskRunSummary.from_dict({**TASK_RUN, "state": "PAUSED"})
|
|
195
|
+
assert run.state == "PAUSED"
|
|
196
|
+
assert not run.is_active
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.parametrize("state", list(TaskRunState))
|
|
200
|
+
def test_only_unfinished_states_are_active(state):
|
|
201
|
+
expected = state.value in {"PENDING", "SUBMITTED", "RUNNING", "CANCELLING"}
|
|
202
|
+
assert TaskRunSummary.from_dict({**TASK_RUN, "state": state.value}).is_active is expected
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_logs_pass_since_and_keep_nanosecond_timestamps(session):
|
|
206
|
+
stamp = "2026-06-04T18:01:12.680610267Z"
|
|
207
|
+
body = {
|
|
208
|
+
"taskRunId": "tr_1",
|
|
209
|
+
"items": [{"timestamp": stamp, "level": "INFO", "message": "hi"}],
|
|
210
|
+
"nextSince": stamp,
|
|
211
|
+
}
|
|
212
|
+
wb = _wb(session, body=body)
|
|
213
|
+
|
|
214
|
+
logs = wb.task_runs.logs("tr_1", since="2026-06-04T18:00:00Z")
|
|
215
|
+
|
|
216
|
+
assert _sent(session)[1:3] == (
|
|
217
|
+
f"{BASE}/task-runs/tr_1/logs",
|
|
218
|
+
{"since": "2026-06-04T18:00:00Z"},
|
|
219
|
+
)
|
|
220
|
+
assert logs.entries[0].timestamp == stamp
|
|
221
|
+
assert logs.next_since == stamp
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_cost_is_a_decimal(session):
|
|
225
|
+
body = {
|
|
226
|
+
"taskRunId": "tr_1",
|
|
227
|
+
"totalCostUsd": "1.10",
|
|
228
|
+
"units": 11,
|
|
229
|
+
"unitType": "RSU",
|
|
230
|
+
"isSettling": True,
|
|
231
|
+
}
|
|
232
|
+
wb = _wb(session, body=body)
|
|
233
|
+
|
|
234
|
+
cost = wb.task_runs.cost("tr_1")
|
|
235
|
+
|
|
236
|
+
assert _sent(session)[1] == f"{BASE}/task-runs/tr_1/cost"
|
|
237
|
+
assert cost.total_cost_usd == Decimal("1.10")
|
|
238
|
+
assert cost.is_settling is True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pytest.mark.parametrize(
|
|
242
|
+
"broken",
|
|
243
|
+
[
|
|
244
|
+
{"totalCostUsd": None},
|
|
245
|
+
{"totalCostUsd": "n/a"},
|
|
246
|
+
{"totalCostUsd": "NaN"},
|
|
247
|
+
{"totalCostUsd": float("inf")},
|
|
248
|
+
{"isSettling": None},
|
|
249
|
+
],
|
|
250
|
+
)
|
|
251
|
+
def test_an_unreadable_cost_is_an_error_not_zero(session, broken):
|
|
252
|
+
body = {"taskRunId": "tr_1", "totalCostUsd": "0", "units": 0, "unitType": "RSU"}
|
|
253
|
+
wb = _wb(session, body={**body, "isSettling": False, **broken})
|
|
254
|
+
with pytest.raises(WherobotsAPIError, match="Task run cost"):
|
|
255
|
+
wb.task_runs.cost("tr_1")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_cancel_posts(session):
|
|
259
|
+
wb = _wb(session)
|
|
260
|
+
|
|
261
|
+
wb.task_runs.cancel("tr_1")
|
|
262
|
+
|
|
263
|
+
assert _sent(session)[:2] == ("POST", f"{BASE}/task-runs/tr_1/cancel")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_a_non_object_body_is_an_api_error(session):
|
|
267
|
+
wb = _wb(session, body=["not", "an", "object"])
|
|
268
|
+
with pytest.raises(WherobotsAPIError, match="unexpected body"):
|
|
269
|
+
wb.task_runs.get("tr_1")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_http_errors_surface_as_api_errors(session):
|
|
273
|
+
resp = MagicMock()
|
|
274
|
+
resp.status_code = 404
|
|
275
|
+
resp.headers = {}
|
|
276
|
+
resp.json.return_value = {"errors": [{"message": "Task not found"}], "requestId": "r"}
|
|
277
|
+
resp.raise_for_status.side_effect = requests.HTTPError(response=resp)
|
|
278
|
+
wb = _wb(session)
|
|
279
|
+
session.request.return_value = resp
|
|
280
|
+
|
|
281
|
+
with pytest.raises(WherobotsAPIError) as err:
|
|
282
|
+
wb.tasks.get("missing")
|
|
283
|
+
assert err.value.status_code == 404
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@given(
|
|
287
|
+
total=st.integers(min_value=0, max_value=500),
|
|
288
|
+
offset=st.integers(min_value=0, max_value=500),
|
|
289
|
+
count=st.integers(min_value=0, max_value=100),
|
|
290
|
+
)
|
|
291
|
+
def test_next_offset_stops_at_the_end(total, offset, count):
|
|
292
|
+
page = OffsetPage(items=[None] * count, total=total, limit=100, offset=offset)
|
|
293
|
+
if page.next_offset is None:
|
|
294
|
+
assert count == 0 or offset + count >= total
|
|
295
|
+
else:
|
|
296
|
+
assert page.next_offset == offset + count < total
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from wherobots.__version__ import __version__
|
|
4
4
|
from wherobots.api.drive import Files, FilesDrive
|
|
5
5
|
from wherobots.api.files import FilesAPI
|
|
6
|
+
from wherobots.api.tasks import TaskRuns, Tasks
|
|
6
7
|
from wherobots.client import WherobotsJob
|
|
7
8
|
from wherobots.config import WherobotsConfig
|
|
8
9
|
from wherobots.enums import (
|
|
@@ -14,6 +15,7 @@ from wherobots.enums import (
|
|
|
14
15
|
JobStatus,
|
|
15
16
|
Region,
|
|
16
17
|
Runtime,
|
|
18
|
+
TaskRunState,
|
|
17
19
|
is_terminal_status,
|
|
18
20
|
)
|
|
19
21
|
from wherobots.exceptions import (
|
|
@@ -34,6 +36,7 @@ from wherobots.models import (
|
|
|
34
36
|
KubeAppEvent,
|
|
35
37
|
LogItem,
|
|
36
38
|
LogsResponse,
|
|
39
|
+
OffsetPage,
|
|
37
40
|
OrganizationCustomer,
|
|
38
41
|
PyPiDependency,
|
|
39
42
|
RunAppMeta,
|
|
@@ -46,6 +49,14 @@ from wherobots.models import (
|
|
|
46
49
|
RunPythonPayload,
|
|
47
50
|
RunView,
|
|
48
51
|
StorageIntegration,
|
|
52
|
+
Task,
|
|
53
|
+
TaskExample,
|
|
54
|
+
TaskRun,
|
|
55
|
+
TaskRunCost,
|
|
56
|
+
TaskRunLogEntry,
|
|
57
|
+
TaskRunLogs,
|
|
58
|
+
TaskRunSummary,
|
|
59
|
+
TaskSummary,
|
|
49
60
|
TemporaryAwsCredentials,
|
|
50
61
|
UtilizationStats,
|
|
51
62
|
)
|
|
@@ -63,6 +74,8 @@ __all__ = [
|
|
|
63
74
|
"WherobotsJob",
|
|
64
75
|
"Job",
|
|
65
76
|
"FilesAPI",
|
|
77
|
+
"Tasks",
|
|
78
|
+
"TaskRuns",
|
|
66
79
|
"WherobotsConfig",
|
|
67
80
|
# Enums
|
|
68
81
|
"Runtime",
|
|
@@ -73,6 +86,7 @@ __all__ = [
|
|
|
73
86
|
"AppType",
|
|
74
87
|
"DependencyType",
|
|
75
88
|
"DependencyFileType",
|
|
89
|
+
"TaskRunState",
|
|
76
90
|
"is_terminal_status",
|
|
77
91
|
# Exceptions
|
|
78
92
|
"WherobotsJobError",
|
|
@@ -105,4 +119,13 @@ __all__ = [
|
|
|
105
119
|
"TemporaryAwsCredentials",
|
|
106
120
|
"UtilizationStats",
|
|
107
121
|
"FileEntry",
|
|
122
|
+
"OffsetPage",
|
|
123
|
+
"TaskSummary",
|
|
124
|
+
"Task",
|
|
125
|
+
"TaskExample",
|
|
126
|
+
"TaskRunSummary",
|
|
127
|
+
"TaskRun",
|
|
128
|
+
"TaskRunLogs",
|
|
129
|
+
"TaskRunLogEntry",
|
|
130
|
+
"TaskRunCost",
|
|
108
131
|
]
|
|
@@ -4,5 +4,6 @@ from wherobots.api.base import BaseClient
|
|
|
4
4
|
from wherobots.api.drive import Files, FilesDrive
|
|
5
5
|
from wherobots.api.files import FilesAPI
|
|
6
6
|
from wherobots.api.runs import RunsAPI
|
|
7
|
+
from wherobots.api.tasks import TaskRuns, Tasks
|
|
7
8
|
|
|
8
|
-
__all__ = ["BaseClient", "Files", "FilesAPI", "FilesDrive", "RunsAPI"]
|
|
9
|
+
__all__ = ["BaseClient", "Files", "FilesAPI", "FilesDrive", "RunsAPI", "TaskRuns", "Tasks"]
|