wherobots-python-sdk 0.3.0__tar.gz → 0.4.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.3.0 → wherobots_python_sdk-0.4.0}/PKG-INFO +57 -18
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/README.md +56 -10
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/pyproject.toml +41 -30
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_api.py +6 -6
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_base_client.py +118 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_client.py +32 -0
- wherobots_python_sdk-0.4.0/tests/test_client_header.py +216 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_config.py +14 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_exceptions.py +2 -3
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_files_api.py +2 -2
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_integration.py +1 -1
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_models.py +17 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_security.py +4 -4
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/__version__.py +1 -1
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/api/base.py +17 -10
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/api/files.py +11 -10
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/api/runs.py +8 -8
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/client.py +34 -22
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/config.py +15 -4
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/exceptions.py +3 -3
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/models.py +22 -20
- wherobots_python_sdk-0.4.0/wherobots/utils/client_header.py +205 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/utils/validation.py +1 -1
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots_python_sdk.egg-info/PKG-INFO +57 -18
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots_python_sdk.egg-info/SOURCES.txt +2 -0
- wherobots_python_sdk-0.4.0/wherobots_python_sdk.egg-info/requires.txt +2 -0
- wherobots_python_sdk-0.3.0/wherobots_python_sdk.egg-info/requires.txt +0 -10
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/LICENSE +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/setup.cfg +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_enums.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_init_exports.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_logger.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_regressions.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/tests/test_utils.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/__init__.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/api/__init__.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/enums.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/py.typed +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/utils/__init__.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots/utils/logger.py +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.0}/wherobots_python_sdk.egg-info/dependency_links.txt +0 -0
- {wherobots_python_sdk-0.3.0 → wherobots_python_sdk-0.4.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.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -23,13 +23,6 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: requests>=2.33.0
|
|
25
25
|
Requires-Dist: boto3>=1.34.0
|
|
26
|
-
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
28
|
-
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
29
|
-
Requires-Dist: pytest-mock>=3.11.1; extra == "dev"
|
|
30
|
-
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
31
|
-
Requires-Dist: mypy>=1.5.0; extra == "dev"
|
|
32
|
-
Requires-Dist: types-requests>=2.31; extra == "dev"
|
|
33
26
|
Dynamic: license-file
|
|
34
27
|
|
|
35
28
|
# Wherobots Python SDK
|
|
@@ -67,11 +60,11 @@ The simplest way to run a local script — only an API key is needed:
|
|
|
67
60
|
from wherobots import WherobotsJob
|
|
68
61
|
|
|
69
62
|
job = WherobotsJob(
|
|
70
|
-
script="my_analysis.py",
|
|
63
|
+
script="my_analysis.py", # local file — auto-uploaded via presigned URL
|
|
71
64
|
name="analysis-job-001",
|
|
72
65
|
runtime="tiny",
|
|
73
66
|
region="aws-us-west-2",
|
|
74
|
-
api_key="wbk_...",
|
|
67
|
+
api_key="wbk_...", # or set WHEROBOTS_API_KEY env var
|
|
75
68
|
)
|
|
76
69
|
|
|
77
70
|
run_id = job.submit()
|
|
@@ -135,6 +128,7 @@ The API key can also be set via the `WHEROBOTS_API_KEY` environment variable.
|
|
|
135
128
|
| `WHEROBOTS_API_BASE_URL` | API base URL | `https://api.cloud.wherobots.com` |
|
|
136
129
|
| `WHEROBOTS_VERSION` | Wherobots version | `latest` |
|
|
137
130
|
| `WHEROBOTS_REQUEST_TIMEOUT_SECONDS`| HTTP request timeout (seconds) | `30` |
|
|
131
|
+
| `WHEROBOTS_CLIENT_CHAIN` | Upstream client-attribution chain (see below) | *(none)* |
|
|
138
132
|
|
|
139
133
|
### WherobotsConfig
|
|
140
134
|
|
|
@@ -164,6 +158,35 @@ config = WherobotsConfig(
|
|
|
164
158
|
| `base_url` | `str` | `"https://api.cloud.wherobots.com"` |
|
|
165
159
|
| `version` | `str` | `"latest"` |
|
|
166
160
|
| `request_timeout_seconds` | `int` | `30` |
|
|
161
|
+
| `client_chain` | `str \| None` | `None` |
|
|
162
|
+
|
|
163
|
+
### Client attribution
|
|
164
|
+
|
|
165
|
+
Every request the SDK sends carries an advisory `X-Wherobots-Client` header so
|
|
166
|
+
Wherobots can attribute workloads to the client that started them:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
X-Wherobots-Client: client=python-sdk;ver=0.3.0;plat=darwin
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The header is an ordered, comma-separated chain of hops — leftmost is the
|
|
173
|
+
origin client, and each component appends its own hop on the right. If your
|
|
174
|
+
tool embeds this SDK, pass your own hop as `client_chain` and the SDK appends
|
|
175
|
+
itself after it:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
job = WherobotsJob(
|
|
179
|
+
script="job.py",
|
|
180
|
+
name="my-job-name",
|
|
181
|
+
client_chain="client=airflow;ver=1.2.0",
|
|
182
|
+
)
|
|
183
|
+
# X-Wherobots-Client: client=airflow;ver=1.2.0, client=python-sdk;ver=0.3.0;plat=linux
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`client_chain` also works on `WherobotsJob.from_run_id(...)` and
|
|
187
|
+
`WherobotsConfig`, or can be set once via `WHEROBOTS_CLIENT_CHAIN`. The header
|
|
188
|
+
is advisory only: it is used for attribution and analytics and never affects
|
|
189
|
+
authentication or authorization.
|
|
167
190
|
|
|
168
191
|
## SDK Reference
|
|
169
192
|
|
|
@@ -192,6 +215,7 @@ WherobotsJob(
|
|
|
192
215
|
base_url: str | None = None, # Override API URL
|
|
193
216
|
request_timeout_seconds: int | None = None, # HTTP timeout
|
|
194
217
|
config: WherobotsConfig | None = None, # Full config object
|
|
218
|
+
client_chain: str | None = None, # Upstream X-Wherobots-Client chain
|
|
195
219
|
)
|
|
196
220
|
```
|
|
197
221
|
|
|
@@ -318,7 +342,7 @@ environment.
|
|
|
318
342
|
|
|
319
343
|
```python
|
|
320
344
|
job = WherobotsJob(
|
|
321
|
-
script="my_job.py",
|
|
345
|
+
script="my_job.py", # local script auto-upload still works
|
|
322
346
|
name="job-with-local-wheel",
|
|
323
347
|
dependencies=[
|
|
324
348
|
# Local wheel — uploaded to managed storage at submit() time
|
|
@@ -368,7 +392,7 @@ with FilesAPI.from_config(WherobotsConfig.from_env()) as files:
|
|
|
368
392
|
)
|
|
369
393
|
|
|
370
394
|
job = WherobotsJob(
|
|
371
|
-
script=script_uri,
|
|
395
|
+
script=script_uri, # already-uploaded URI
|
|
372
396
|
name="job-in-integration",
|
|
373
397
|
dependencies=[WherobotsJob.add_file_dependency(wheel_uri)],
|
|
374
398
|
)
|
|
@@ -628,10 +652,8 @@ Base URL: `https://api.cloud.wherobots.com`
|
|
|
628
652
|
```bash
|
|
629
653
|
git clone https://github.com/wherobots/wherobots-python-sdk.git
|
|
630
654
|
cd wherobots-python-sdk
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
pip install -e ".[dev]"
|
|
634
|
-
pre-commit install
|
|
655
|
+
uv sync --group dev
|
|
656
|
+
uv run pre-commit install
|
|
635
657
|
```
|
|
636
658
|
|
|
637
659
|
### Quality checks
|
|
@@ -639,10 +661,27 @@ pre-commit install
|
|
|
639
661
|
```bash
|
|
640
662
|
make lint # ruff check + ruff format --check
|
|
641
663
|
make format # auto-fix lint issues and reformat
|
|
642
|
-
make typecheck #
|
|
643
|
-
make
|
|
664
|
+
make typecheck # pyrefly (strict config)
|
|
665
|
+
make rails # import-linter + no-suppression + ast-grep
|
|
666
|
+
make check # lint + typecheck + rails + tests + build
|
|
644
667
|
```
|
|
645
668
|
|
|
669
|
+
### Suppression baseline
|
|
670
|
+
|
|
671
|
+
Pre-existing debt is grandfathered in `.slop-suppressions.json` (CODEOWNED): the inline
|
|
672
|
+
suppressions already in the tree, plus the `# noqa: CODE` / `# pyrefly: ignore` comments that
|
|
673
|
+
`ruff check --add-noqa` and `pyrefly check --suppress-errors` wrote for pre-existing violations.
|
|
674
|
+
The suppressions are functional at the tool level; the keystone
|
|
675
|
+
(`.slop-repellant/check_no_suppression.py`) fails any net-new one.
|
|
676
|
+
|
|
677
|
+
- **Prune** after fixing baselined code (drops stale entries):
|
|
678
|
+
- ruff's `RUF100` auto-removes stale noqas via the pre-commit hook's `--fix`
|
|
679
|
+
- `uv run pyrefly suppress --remove-unused`
|
|
680
|
+
- then `uv run python .slop-repellant/check_no_suppression.py --update`
|
|
681
|
+
- **Reset** (re-record from scratch): delete `.slop-suppressions.json`, then run the same `--update`.
|
|
682
|
+
- Shrinking the baseline is always welcome; **growing it requires CODEOWNER sign-off** — fix the
|
|
683
|
+
code instead of widening the baseline.
|
|
684
|
+
|
|
646
685
|
### Releasing
|
|
647
686
|
|
|
648
687
|
See [`RELEASING.md`](RELEASING.md) for the release flow (version
|
|
@@ -33,11 +33,11 @@ The simplest way to run a local script — only an API key is needed:
|
|
|
33
33
|
from wherobots import WherobotsJob
|
|
34
34
|
|
|
35
35
|
job = WherobotsJob(
|
|
36
|
-
script="my_analysis.py",
|
|
36
|
+
script="my_analysis.py", # local file — auto-uploaded via presigned URL
|
|
37
37
|
name="analysis-job-001",
|
|
38
38
|
runtime="tiny",
|
|
39
39
|
region="aws-us-west-2",
|
|
40
|
-
api_key="wbk_...",
|
|
40
|
+
api_key="wbk_...", # or set WHEROBOTS_API_KEY env var
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
run_id = job.submit()
|
|
@@ -101,6 +101,7 @@ The API key can also be set via the `WHEROBOTS_API_KEY` environment variable.
|
|
|
101
101
|
| `WHEROBOTS_API_BASE_URL` | API base URL | `https://api.cloud.wherobots.com` |
|
|
102
102
|
| `WHEROBOTS_VERSION` | Wherobots version | `latest` |
|
|
103
103
|
| `WHEROBOTS_REQUEST_TIMEOUT_SECONDS`| HTTP request timeout (seconds) | `30` |
|
|
104
|
+
| `WHEROBOTS_CLIENT_CHAIN` | Upstream client-attribution chain (see below) | *(none)* |
|
|
104
105
|
|
|
105
106
|
### WherobotsConfig
|
|
106
107
|
|
|
@@ -130,6 +131,35 @@ config = WherobotsConfig(
|
|
|
130
131
|
| `base_url` | `str` | `"https://api.cloud.wherobots.com"` |
|
|
131
132
|
| `version` | `str` | `"latest"` |
|
|
132
133
|
| `request_timeout_seconds` | `int` | `30` |
|
|
134
|
+
| `client_chain` | `str \| None` | `None` |
|
|
135
|
+
|
|
136
|
+
### Client attribution
|
|
137
|
+
|
|
138
|
+
Every request the SDK sends carries an advisory `X-Wherobots-Client` header so
|
|
139
|
+
Wherobots can attribute workloads to the client that started them:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
X-Wherobots-Client: client=python-sdk;ver=0.3.0;plat=darwin
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The header is an ordered, comma-separated chain of hops — leftmost is the
|
|
146
|
+
origin client, and each component appends its own hop on the right. If your
|
|
147
|
+
tool embeds this SDK, pass your own hop as `client_chain` and the SDK appends
|
|
148
|
+
itself after it:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
job = WherobotsJob(
|
|
152
|
+
script="job.py",
|
|
153
|
+
name="my-job-name",
|
|
154
|
+
client_chain="client=airflow;ver=1.2.0",
|
|
155
|
+
)
|
|
156
|
+
# X-Wherobots-Client: client=airflow;ver=1.2.0, client=python-sdk;ver=0.3.0;plat=linux
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`client_chain` also works on `WherobotsJob.from_run_id(...)` and
|
|
160
|
+
`WherobotsConfig`, or can be set once via `WHEROBOTS_CLIENT_CHAIN`. The header
|
|
161
|
+
is advisory only: it is used for attribution and analytics and never affects
|
|
162
|
+
authentication or authorization.
|
|
133
163
|
|
|
134
164
|
## SDK Reference
|
|
135
165
|
|
|
@@ -158,6 +188,7 @@ WherobotsJob(
|
|
|
158
188
|
base_url: str | None = None, # Override API URL
|
|
159
189
|
request_timeout_seconds: int | None = None, # HTTP timeout
|
|
160
190
|
config: WherobotsConfig | None = None, # Full config object
|
|
191
|
+
client_chain: str | None = None, # Upstream X-Wherobots-Client chain
|
|
161
192
|
)
|
|
162
193
|
```
|
|
163
194
|
|
|
@@ -284,7 +315,7 @@ environment.
|
|
|
284
315
|
|
|
285
316
|
```python
|
|
286
317
|
job = WherobotsJob(
|
|
287
|
-
script="my_job.py",
|
|
318
|
+
script="my_job.py", # local script auto-upload still works
|
|
288
319
|
name="job-with-local-wheel",
|
|
289
320
|
dependencies=[
|
|
290
321
|
# Local wheel — uploaded to managed storage at submit() time
|
|
@@ -334,7 +365,7 @@ with FilesAPI.from_config(WherobotsConfig.from_env()) as files:
|
|
|
334
365
|
)
|
|
335
366
|
|
|
336
367
|
job = WherobotsJob(
|
|
337
|
-
script=script_uri,
|
|
368
|
+
script=script_uri, # already-uploaded URI
|
|
338
369
|
name="job-in-integration",
|
|
339
370
|
dependencies=[WherobotsJob.add_file_dependency(wheel_uri)],
|
|
340
371
|
)
|
|
@@ -594,10 +625,8 @@ Base URL: `https://api.cloud.wherobots.com`
|
|
|
594
625
|
```bash
|
|
595
626
|
git clone https://github.com/wherobots/wherobots-python-sdk.git
|
|
596
627
|
cd wherobots-python-sdk
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
pip install -e ".[dev]"
|
|
600
|
-
pre-commit install
|
|
628
|
+
uv sync --group dev
|
|
629
|
+
uv run pre-commit install
|
|
601
630
|
```
|
|
602
631
|
|
|
603
632
|
### Quality checks
|
|
@@ -605,10 +634,27 @@ pre-commit install
|
|
|
605
634
|
```bash
|
|
606
635
|
make lint # ruff check + ruff format --check
|
|
607
636
|
make format # auto-fix lint issues and reformat
|
|
608
|
-
make typecheck #
|
|
609
|
-
make
|
|
637
|
+
make typecheck # pyrefly (strict config)
|
|
638
|
+
make rails # import-linter + no-suppression + ast-grep
|
|
639
|
+
make check # lint + typecheck + rails + tests + build
|
|
610
640
|
```
|
|
611
641
|
|
|
642
|
+
### Suppression baseline
|
|
643
|
+
|
|
644
|
+
Pre-existing debt is grandfathered in `.slop-suppressions.json` (CODEOWNED): the inline
|
|
645
|
+
suppressions already in the tree, plus the `# noqa: CODE` / `# pyrefly: ignore` comments that
|
|
646
|
+
`ruff check --add-noqa` and `pyrefly check --suppress-errors` wrote for pre-existing violations.
|
|
647
|
+
The suppressions are functional at the tool level; the keystone
|
|
648
|
+
(`.slop-repellant/check_no_suppression.py`) fails any net-new one.
|
|
649
|
+
|
|
650
|
+
- **Prune** after fixing baselined code (drops stale entries):
|
|
651
|
+
- ruff's `RUF100` auto-removes stale noqas via the pre-commit hook's `--fix`
|
|
652
|
+
- `uv run pyrefly suppress --remove-unused`
|
|
653
|
+
- then `uv run python .slop-repellant/check_no_suppression.py --update`
|
|
654
|
+
- **Reset** (re-record from scratch): delete `.slop-suppressions.json`, then run the same `--update`.
|
|
655
|
+
- Shrinking the baseline is always welcome; **growing it requires CODEOWNER sign-off** — fix the
|
|
656
|
+
code instead of widening the baseline.
|
|
657
|
+
|
|
612
658
|
### Releasing
|
|
613
659
|
|
|
614
660
|
See [`RELEASING.md`](RELEASING.md) for the release flow (version
|
|
@@ -35,13 +35,19 @@ Repository = "https://github.com/wherobots/wherobots-python-sdk"
|
|
|
35
35
|
Issues = "https://github.com/wherobots/wherobots-python-sdk/issues"
|
|
36
36
|
Documentation = "https://docs.wherobots.com"
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
# PEP 735 dev dependencies — installed with `uv sync --group dev`.
|
|
39
|
+
[dependency-groups]
|
|
39
40
|
dev = [
|
|
40
|
-
"pytest>=
|
|
41
|
+
"pytest>=8.0",
|
|
41
42
|
"pytest-cov>=4.1.0",
|
|
42
43
|
"pytest-mock>=3.11.1",
|
|
43
|
-
"ruff>=0.
|
|
44
|
-
"
|
|
44
|
+
"ruff>=0.15,<0.17",
|
|
45
|
+
"pyrefly>=0.16",
|
|
46
|
+
"import-linter>=2.1",
|
|
47
|
+
"pre-commit>=3.8",
|
|
48
|
+
"hypothesis>=6.100",
|
|
49
|
+
"cosmic-ray>=8.4",
|
|
50
|
+
"ast-grep-cli>=0.39",
|
|
45
51
|
"types-requests>=2.31",
|
|
46
52
|
]
|
|
47
53
|
|
|
@@ -54,44 +60,49 @@ wherobots = ["py.typed"]
|
|
|
54
60
|
[tool.setuptools.dynamic]
|
|
55
61
|
version = {attr = "wherobots.__version__.__version__"}
|
|
56
62
|
|
|
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.
|
|
65
|
+
# NOTE: `lint.select`/`lint.ignore` would REPLACE the base's — tune only via
|
|
66
|
+
# the extend-* keys.
|
|
57
67
|
[tool.ruff]
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
extend = ".slop-repellant/ruff-base.toml"
|
|
69
|
+
# .slop-repellant/ is vendored, kit-MANAGED tooling (re-rendered on
|
|
70
|
+
# slop-repellant-update) — linting it locally would fork the managed files.
|
|
71
|
+
extend-exclude = ["env", ".venv", "venv", "build", "dist", ".slop-repellant"]
|
|
61
72
|
|
|
62
73
|
[tool.ruff.lint]
|
|
63
|
-
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
64
74
|
# SIM117 merges nested with-statements — often hurts readability when
|
|
65
75
|
# the context managers are logically distinct. Disable.
|
|
66
|
-
|
|
76
|
+
# EM101/EM102/TRY003 (exception-message-as-literal trio): style position,
|
|
77
|
+
# not debt — we keep messages inline at the raise site. Disable globally.
|
|
78
|
+
# PLR0917 (too-many-positional-arguments): newly enabled in ruff 0.16's ALL
|
|
79
|
+
# set. It is the positional-only variant of PLR0913 (too-many-arguments),
|
|
80
|
+
# which the few wide signatures here already carry an accepted `# noqa:
|
|
81
|
+
# PLR0913` for — so PLR0917 is redundant with a complaint the codebase has
|
|
82
|
+
# already signed off on. Ignored globally rather than making those args
|
|
83
|
+
# keyword-only, which would be a semver-breaking public-API change.
|
|
84
|
+
extend-ignore = ["SIM117", "EM101", "EM102", "TRY003", "PLR0917"]
|
|
67
85
|
|
|
68
|
-
[tool.ruff.lint.per-file-ignores]
|
|
86
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
69
87
|
# Tests may assert freely, re-raise without chaining, shadow builtins
|
|
70
88
|
# for fixtures, and use mock call patterns that look like unused
|
|
71
|
-
# locals.
|
|
72
|
-
|
|
89
|
+
# locals. N803/N806 fire on @patch-injected MockSession-style names and
|
|
90
|
+
# PLC0415 on deliberate deferred imports — idiomatic in tests, not debt.
|
|
91
|
+
# (Extends the base's tests/** ignore set.)
|
|
92
|
+
"tests/**/*.py" = ["B011", "B904", "F841", "RUF059", "RUF013", "N803", "N806", "PLC0415"]
|
|
93
|
+
# e2e/ holds standalone CLI scripts: printing is their job (T201), and
|
|
94
|
+
# they are intentionally not a package (INP001) with exec bits (EXE001).
|
|
95
|
+
"e2e/**/*.py" = ["T201", "INP001", "EXE001"]
|
|
73
96
|
# Package __init__ groups exports by category with comment headers;
|
|
74
97
|
# don't let ruff re-sort them alphabetically.
|
|
75
98
|
"wherobots/__init__.py" = ["RUF022"]
|
|
76
99
|
|
|
77
|
-
[tool.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
[[tool.mypy.overrides]]
|
|
85
|
-
# Test code is not held to strict typing — productionize incrementally.
|
|
86
|
-
module = "tests.*"
|
|
87
|
-
ignore_errors = true
|
|
88
|
-
|
|
89
|
-
[[tool.mypy.overrides]]
|
|
90
|
-
# boto3 / botocore ship runtime types in some versions but the
|
|
91
|
-
# pre-commit mypy env doesn't install them. Declare the policy once
|
|
92
|
-
# here rather than scattering per-import ``# type: ignore`` comments.
|
|
93
|
-
module = ["boto3", "boto3.*", "botocore", "botocore.*"]
|
|
94
|
-
ignore_missing_imports = true
|
|
100
|
+
[tool.pyrefly]
|
|
101
|
+
project-includes = ["wherobots"]
|
|
102
|
+
check-unannotated-defs = true
|
|
103
|
+
strict-callable-subtyping = true
|
|
104
|
+
permissive-ignores = false
|
|
105
|
+
ignore-errors-in-generated-code = false
|
|
95
106
|
|
|
96
107
|
[tool.pytest.ini_options]
|
|
97
108
|
testpaths = ["tests"]
|
|
@@ -49,7 +49,7 @@ def _make_response(json_data, status_code=200):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
# =========================================================================== #
|
|
52
|
-
# list()
|
|
52
|
+
# list() # noqa: ERA001
|
|
53
53
|
# =========================================================================== #
|
|
54
54
|
|
|
55
55
|
|
|
@@ -120,7 +120,7 @@ class TestList:
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
# =========================================================================== #
|
|
123
|
-
# create()
|
|
123
|
+
# create() # noqa: ERA001
|
|
124
124
|
# =========================================================================== #
|
|
125
125
|
|
|
126
126
|
|
|
@@ -177,7 +177,7 @@ class TestCreate:
|
|
|
177
177
|
|
|
178
178
|
|
|
179
179
|
# =========================================================================== #
|
|
180
|
-
# get()
|
|
180
|
+
# get() # noqa: ERA001
|
|
181
181
|
# =========================================================================== #
|
|
182
182
|
|
|
183
183
|
|
|
@@ -197,7 +197,7 @@ class TestGet:
|
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
# =========================================================================== #
|
|
200
|
-
# cancel()
|
|
200
|
+
# cancel() # noqa: ERA001
|
|
201
201
|
# =========================================================================== #
|
|
202
202
|
|
|
203
203
|
|
|
@@ -213,7 +213,7 @@ class TestCancel:
|
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
# =========================================================================== #
|
|
216
|
-
# get_logs()
|
|
216
|
+
# get_logs() # noqa: ERA001
|
|
217
217
|
# =========================================================================== #
|
|
218
218
|
|
|
219
219
|
|
|
@@ -241,7 +241,7 @@ class TestGetLogs:
|
|
|
241
241
|
|
|
242
242
|
|
|
243
243
|
# =========================================================================== #
|
|
244
|
-
# get_metrics()
|
|
244
|
+
# get_metrics() # noqa: ERA001
|
|
245
245
|
# =========================================================================== #
|
|
246
246
|
|
|
247
247
|
|
|
@@ -8,6 +8,12 @@ import requests
|
|
|
8
8
|
from wherobots.api.base import BaseClient
|
|
9
9
|
from wherobots.config import WherobotsConfig
|
|
10
10
|
from wherobots.exceptions import WherobotsAPIError, WherobotsConfigError
|
|
11
|
+
from wherobots.utils.client_header import (
|
|
12
|
+
CLIENT_HEADER,
|
|
13
|
+
build_hop,
|
|
14
|
+
resolve_platform,
|
|
15
|
+
resolve_version,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
# ---------------------------------------------------------------------------
|
|
13
19
|
# Fixtures
|
|
@@ -34,6 +40,118 @@ class TestAuthHeaders:
|
|
|
34
40
|
assert "Authorization" not in headers
|
|
35
41
|
|
|
36
42
|
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Client attribution (X-Wherobots-Client)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestClientAttributionHeader:
|
|
49
|
+
@patch("wherobots.api.base.requests.Session")
|
|
50
|
+
def test_header_set_on_the_session(self, MockSession, config):
|
|
51
|
+
session = MockSession.return_value
|
|
52
|
+
session.headers = {}
|
|
53
|
+
session.mount = MagicMock()
|
|
54
|
+
|
|
55
|
+
BaseClient(config)
|
|
56
|
+
|
|
57
|
+
assert session.headers[CLIENT_HEADER] == build_hop()
|
|
58
|
+
assert session.headers[CLIENT_HEADER] == (
|
|
59
|
+
f"client=python-sdk;ver={resolve_version()};plat={resolve_platform()}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@patch("wherobots.api.base.requests.Session")
|
|
63
|
+
def test_header_rides_along_with_every_request(self, MockSession, config):
|
|
64
|
+
# The header lives on the session rather than on individual calls,
|
|
65
|
+
# so it is attached to whatever requests the client later makes.
|
|
66
|
+
session = MockSession.return_value
|
|
67
|
+
session.headers = {}
|
|
68
|
+
session.mount = MagicMock()
|
|
69
|
+
|
|
70
|
+
mock_response = MagicMock()
|
|
71
|
+
mock_response.raise_for_status.return_value = None
|
|
72
|
+
session.request.return_value = mock_response
|
|
73
|
+
|
|
74
|
+
client = BaseClient(config)
|
|
75
|
+
client.get("/runs")
|
|
76
|
+
client.post("/runs", json_body={"name": "test"})
|
|
77
|
+
|
|
78
|
+
assert session.request.call_count == 2
|
|
79
|
+
assert session.headers[CLIENT_HEADER] == build_hop()
|
|
80
|
+
|
|
81
|
+
@patch("wherobots.api.base.requests.Session")
|
|
82
|
+
def test_upstream_chain_is_appended_to_not_replaced(self, MockSession):
|
|
83
|
+
cfg = WherobotsConfig(
|
|
84
|
+
api_key="test-key-123",
|
|
85
|
+
base_url="https://api.test.wherobots.com",
|
|
86
|
+
client_chain="client=airflow;ver=1.2.0",
|
|
87
|
+
)
|
|
88
|
+
session = MockSession.return_value
|
|
89
|
+
session.headers = {}
|
|
90
|
+
session.mount = MagicMock()
|
|
91
|
+
|
|
92
|
+
BaseClient(cfg)
|
|
93
|
+
|
|
94
|
+
assert session.headers[CLIENT_HEADER] == f"client=airflow;ver=1.2.0, {build_hop()}"
|
|
95
|
+
|
|
96
|
+
@patch("wherobots.api.base.requests.Session")
|
|
97
|
+
def test_attribution_never_displaces_auth(self, MockSession):
|
|
98
|
+
# A CRLF is the only thing in a chain that could ever *become* another
|
|
99
|
+
# header, so that is what this has to carry: a plain comma-separated
|
|
100
|
+
# "X-API-Key: nope" is just more text inside one header value and could
|
|
101
|
+
# not displace auth no matter how the chain were handled.
|
|
102
|
+
cfg = WherobotsConfig(
|
|
103
|
+
api_key="test-key-123",
|
|
104
|
+
base_url="https://api.test.wherobots.com",
|
|
105
|
+
client_chain="client=spoofed\r\nX-API-Key: nope",
|
|
106
|
+
)
|
|
107
|
+
session = MockSession.return_value
|
|
108
|
+
session.headers = {}
|
|
109
|
+
session.mount = MagicMock()
|
|
110
|
+
|
|
111
|
+
BaseClient(cfg)
|
|
112
|
+
|
|
113
|
+
assert session.headers["X-API-Key"] == "test-key-123"
|
|
114
|
+
assert "\r" not in session.headers[CLIENT_HEADER]
|
|
115
|
+
assert "\n" not in session.headers[CLIENT_HEADER]
|
|
116
|
+
|
|
117
|
+
def test_a_hostile_upstream_chain_still_yields_a_sendable_request(self):
|
|
118
|
+
"""The chain is caller-supplied, so it must not be able to break a call.
|
|
119
|
+
|
|
120
|
+
`requests` refuses to send a header value containing a control
|
|
121
|
+
character, raising `InvalidHeader` — a `requests` exception, not a
|
|
122
|
+
`WherobotsJobError`. Before the chain was scrubbed, an upstream that
|
|
123
|
+
forwarded a CR or LF broke every request this SDK made and leaked a
|
|
124
|
+
foreign exception type doing it.
|
|
125
|
+
|
|
126
|
+
Non-ASCII text gets past `requests` entirely and then fails one layer
|
|
127
|
+
lower, where `http.client.putheader` latin-1 encodes, so both are
|
|
128
|
+
exercised here and the latin-1 encode is asserted explicitly.
|
|
129
|
+
|
|
130
|
+
This asserts against real header preparation rather than a mocked
|
|
131
|
+
session, because the mock is exactly what hid the defect.
|
|
132
|
+
"""
|
|
133
|
+
cfg = WherobotsConfig(
|
|
134
|
+
api_key="test-key-123",
|
|
135
|
+
base_url="https://api.test.wherobots.com",
|
|
136
|
+
client_chain="client=evil\r\nX-Injected: yes, client=日本語",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
with patch("wherobots.api.base.requests.Session") as MockSession:
|
|
140
|
+
session = MockSession.return_value
|
|
141
|
+
session.headers = {}
|
|
142
|
+
session.mount = MagicMock()
|
|
143
|
+
BaseClient(cfg)
|
|
144
|
+
|
|
145
|
+
prepared = requests.Request(
|
|
146
|
+
"GET", "https://api.test.wherobots.com/runs", headers=session.headers
|
|
147
|
+
).prepare()
|
|
148
|
+
|
|
149
|
+
assert "X-Injected" not in prepared.headers
|
|
150
|
+
assert prepared.headers[CLIENT_HEADER].startswith("client=evil")
|
|
151
|
+
# What http.client actually does with it.
|
|
152
|
+
prepared.headers[CLIENT_HEADER].encode("latin-1")
|
|
153
|
+
|
|
154
|
+
|
|
37
155
|
# ---------------------------------------------------------------------------
|
|
38
156
|
# Config validation
|
|
39
157
|
# ---------------------------------------------------------------------------
|
|
@@ -821,3 +821,35 @@ class TestScriptAutoUpload:
|
|
|
821
821
|
assert uri == "s3://managed/scripts/job.py"
|
|
822
822
|
mock_files.upload_managed_file.assert_called_once_with("job.py", subdir="scripts")
|
|
823
823
|
mock_files.upload_dependency.assert_not_called()
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
# ── Client attribution chain plumbing ──────────────────────────────────
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
class TestClientChainPlumbing:
|
|
830
|
+
"""``client_chain`` reaches the config every construction path uses."""
|
|
831
|
+
|
|
832
|
+
def test_constructor_threads_client_chain_into_config(self, mock_env):
|
|
833
|
+
job = WherobotsJob(
|
|
834
|
+
script="s3://bucket/main.py",
|
|
835
|
+
name="chain-job-name",
|
|
836
|
+
runtime="tiny",
|
|
837
|
+
client_chain="client=airflow;ver=1.2.0",
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
assert job._config.client_chain == "client=airflow;ver=1.2.0"
|
|
841
|
+
|
|
842
|
+
def test_constructor_defaults_to_no_upstream_chain(self, mock_env, monkeypatch):
|
|
843
|
+
monkeypatch.delenv("WHEROBOTS_CLIENT_CHAIN", raising=False)
|
|
844
|
+
|
|
845
|
+
job = WherobotsJob(script="s3://bucket/main.py", name="chain-job-name", runtime="tiny")
|
|
846
|
+
|
|
847
|
+
assert job._config.client_chain is None
|
|
848
|
+
|
|
849
|
+
def test_from_run_id_threads_client_chain_into_config(self, mock_env, mock_run_view):
|
|
850
|
+
with patch("wherobots.client.RunsAPI") as runs_cls:
|
|
851
|
+
runs_cls.from_config.return_value = _make_mock_api(run_view=mock_run_view)
|
|
852
|
+
|
|
853
|
+
job = WherobotsJob.from_run_id("run-123", client_chain="client=airflow;ver=1.2.0")
|
|
854
|
+
|
|
855
|
+
assert job._config.client_chain == "client=airflow;ver=1.2.0"
|