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.
Files changed (48) hide show
  1. {wherobots_python_sdk-0.5.1/wherobots_python_sdk.egg-info → wherobots_python_sdk-0.7.0}/PKG-INFO +66 -8
  2. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/README.md +63 -4
  3. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/pyproject.toml +18 -17
  4. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_base_client.py +57 -4
  5. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_config.py +12 -0
  6. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_files_api.py +16 -3
  7. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_models.py +4 -4
  8. wherobots_python_sdk-0.7.0/tests/test_tasks.py +296 -0
  9. wherobots_python_sdk-0.7.0/tests/test_workload_auth.py +151 -0
  10. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/__init__.py +23 -0
  11. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/__version__.py +1 -1
  12. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/__init__.py +2 -1
  13. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/base.py +30 -7
  14. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/drive.py +4 -18
  15. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/files.py +8 -1
  16. wherobots_python_sdk-0.7.0/wherobots/api/tasks.py +265 -0
  17. wherobots_python_sdk-0.7.0/wherobots/api/workload_auth.py +116 -0
  18. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/config.py +12 -2
  19. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/enums.py +33 -0
  20. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/exceptions.py +1 -1
  21. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/models.py +347 -10
  22. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/sdk.py +18 -0
  23. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0/wherobots_python_sdk.egg-info}/PKG-INFO +66 -8
  24. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots_python_sdk.egg-info/SOURCES.txt +4 -0
  25. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots_python_sdk.egg-info/requires.txt +1 -1
  26. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/LICENSE +0 -0
  27. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/setup.cfg +0 -0
  28. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_api.py +0 -0
  29. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_client.py +0 -0
  30. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_client_header.py +0 -0
  31. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_enums.py +0 -0
  32. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_exceptions.py +0 -0
  33. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_files_drive.py +0 -0
  34. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_init_exports.py +0 -0
  35. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_integration.py +0 -0
  36. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_logger.py +0 -0
  37. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_regressions.py +0 -0
  38. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_security.py +0 -0
  39. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/tests/test_utils.py +0 -0
  40. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/api/runs.py +0 -0
  41. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/client.py +0 -0
  42. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/py.typed +0 -0
  43. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/utils/__init__.py +0 -0
  44. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/utils/client_header.py +0 -0
  45. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/utils/logger.py +0 -0
  46. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots/utils/validation.py +0 -0
  47. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.0}/wherobots_python_sdk.egg-info/dependency_links.txt +0 -0
  48. {wherobots_python_sdk-0.5.1 → wherobots_python_sdk-0.7.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.5.1
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.10
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.43.108
24
+ Requires-Dist: boto3>=1.34.0
26
25
  Dynamic: license-file
27
26
 
28
27
  # Wherobots Python SDK
29
28
 
30
29
  [![CI](https://github.com/wherobots/wherobots-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/wherobots/wherobots-python-sdk/actions/workflows/ci.yml)
31
- [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
30
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
32
31
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
33
32
  [![Version](https://img.shields.io/pypi/v/wherobots-python-sdk?color=orange)](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 API key was not accepted | `WherobotsNotAuthenticatedError` |
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 API key was not accepted (HTTP 401). Subclass of `WherobotsAPIError`. |
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
  [![CI](https://github.com/wherobots/wherobots-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/wherobots/wherobots-python-sdk/actions/workflows/ci.yml)
4
- [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
4
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
5
5
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
6
6
  [![Version](https://img.shields.io/pypi/v/wherobots-python-sdk?color=orange)](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 API key was not accepted | `WherobotsNotAuthenticatedError` |
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 API key was not accepted (HTTP 401). Subclass of `WherobotsAPIError`. |
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>=83.0.0", "wheel"]
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"
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.43.108",
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>=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",
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, py310 target, select = ["ALL"]
64
- # plus the typing.Any/typing.cast ban all come from the base config.
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 test_missing_api_key_raises(self):
161
+ def test_missing_credentials_raises(self):
162
162
  cfg = WherobotsConfig(api_key=None)
163
- with pytest.raises(WherobotsConfigError, match="API key required"):
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="API key required"):
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, timezone
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 WherobotsAPIError, WherobotsValidationError
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(timezone.utc) + timedelta(hours=1)).isoformat()
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, timezone # noqa: E402
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(timezone.utc) - timedelta(seconds=1)
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(timezone.utc) + timedelta(seconds=30)
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=timezone.utc),
1037
+ expiration=datetime(2026, 1, 1, tzinfo=UTC),
1038
1038
  region="us-west-2",
1039
1039
  )
1040
1040
  rendered = repr(creds)