wherobots-python-sdk 0.2.1__tar.gz → 0.3.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.2.1 → wherobots_python_sdk-0.3.0}/PKG-INFO +78 -3
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/README.md +76 -2
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/pyproject.toml +8 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_client.py +164 -8
- wherobots_python_sdk-0.3.0/tests/test_files_api.py +735 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_init_exports.py +8 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_models.py +111 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/__init__.py +4 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/__version__.py +1 -1
- wherobots_python_sdk-0.3.0/wherobots/api/files.py +579 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/client.py +88 -17
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/models.py +132 -3
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots_python_sdk.egg-info/PKG-INFO +78 -3
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots_python_sdk.egg-info/requires.txt +1 -0
- wherobots_python_sdk-0.2.1/tests/test_files_api.py +0 -622
- wherobots_python_sdk-0.2.1/wherobots/api/files.py +0 -386
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/LICENSE +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/setup.cfg +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_api.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_base_client.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_config.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_enums.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_exceptions.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_integration.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_logger.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_regressions.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_security.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/tests/test_utils.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/api/__init__.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/api/base.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/api/runs.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/config.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/enums.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/exceptions.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/py.typed +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/utils/__init__.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/utils/logger.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots/utils/validation.py +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots_python_sdk.egg-info/SOURCES.txt +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.0}/wherobots_python_sdk.egg-info/dependency_links.txt +0 -0
- {wherobots_python_sdk-0.2.1 → wherobots_python_sdk-0.3.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.3.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
|
|
@@ -22,6 +22,7 @@ Requires-Python: >=3.10
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: requests>=2.33.0
|
|
25
|
+
Requires-Dist: boto3>=1.34.0
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
27
28
|
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
@@ -187,7 +188,7 @@ WherobotsJob(
|
|
|
187
188
|
spark_driver_disk_gb: int | None = None, # Driver disk size (GB)
|
|
188
189
|
spark_executor_disk_gb: int | None = None, # Executor disk size (GB)
|
|
189
190
|
jar_main_class: str | None = None, # Main class (required for JARs)
|
|
190
|
-
auto_upload: bool = True, # Upload local files to S3
|
|
191
|
+
auto_upload: bool = True, # Upload local files to S3 (managed storage)
|
|
191
192
|
base_url: str | None = None, # Override API URL
|
|
192
193
|
request_timeout_seconds: int | None = None, # HTTP timeout
|
|
193
194
|
config: WherobotsConfig | None = None, # Full config object
|
|
@@ -235,7 +236,7 @@ with WherobotsJob(script="s3://bucket/script.py", name="my-job") as job:
|
|
|
235
236
|
| `from_run_id(run_id, api_key=None, ...)` | `WherobotsJob` | Attach to an existing run for read-only log/metric access. No script required. `submit()` is disabled on the returned instance. |
|
|
236
237
|
| `list_runs(...)` | `RunListPage` | List runs with optional filters. No instance required. |
|
|
237
238
|
| `add_pypi_dependency(name, version)` | `dict` | Create a PyPI dependency dict for the `dependencies` parameter. |
|
|
238
|
-
| `add_file_dependency(file_path)` | `dict` | Create a file dependency dict (`.jar`, `.whl`, `.zip`, `.json`). |
|
|
239
|
+
| `add_file_dependency(file_path)` | `dict` | Create a file dependency dict (`.jar`, `.whl`, `.zip`, `.json`). Accepts an `s3://` URI or a local path — local paths are auto-uploaded to **managed storage** at `submit()` time. For non-managed destinations, upload via `FilesAPI` first and pass the resulting `s3://` URI. |
|
|
239
240
|
|
|
240
241
|
#### Attaching to an Existing Run
|
|
241
242
|
|
|
@@ -307,6 +308,80 @@ job = WherobotsJob(
|
|
|
307
308
|
)
|
|
308
309
|
```
|
|
309
310
|
|
|
311
|
+
##### Local Dependency Files
|
|
312
|
+
|
|
313
|
+
`add_file_dependency` also accepts a **local path** to a `.whl`, `.jar`, `.zip`,
|
|
314
|
+
or `.json`. The SDK uploads the file just before the job is submitted, fetching
|
|
315
|
+
short-lived AWS STS credentials from the Wherobots API (`POST /storage/credentials`)
|
|
316
|
+
and using boto3 multipart under the hood — no AWS credentials need to leave your
|
|
317
|
+
environment.
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
job = WherobotsJob(
|
|
321
|
+
script="my_job.py", # local script auto-upload still works
|
|
322
|
+
name="job-with-local-wheel",
|
|
323
|
+
dependencies=[
|
|
324
|
+
# Local wheel — uploaded to managed storage at submit() time
|
|
325
|
+
WherobotsJob.add_file_dependency("dist/mypkg-0.1.0-py3-none-any.whl"),
|
|
326
|
+
# Already-uploaded artifact — used as-is
|
|
327
|
+
WherobotsJob.add_file_dependency("s3://bucket/shared-libs/lib-2.0.jar"),
|
|
328
|
+
WherobotsJob.add_pypi_dependency("pandas", "2.0.3"),
|
|
329
|
+
],
|
|
330
|
+
)
|
|
331
|
+
job.submit()
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Auto-upload always targets the org's **managed storage**:
|
|
335
|
+
|
|
336
|
+
| Artifact | Destination |
|
|
337
|
+
|---|---|
|
|
338
|
+
| Script (when `script` is a local path) | `s3://<managed.path>/scripts/<filename>` |
|
|
339
|
+
| Dependency (when path is a local file) | `s3://<managed.path>/dependencies/<filename>` |
|
|
340
|
+
|
|
341
|
+
Local dependency uploads honor the same `auto_upload` flag as the script —
|
|
342
|
+
if `auto_upload=False`, attempting to submit a job with a local dependency
|
|
343
|
+
path raises `WherobotsValidationError`. The 5 GiB cap on a single upload
|
|
344
|
+
applies; for larger artifacts, pre-upload and pass an `s3://` URI.
|
|
345
|
+
|
|
346
|
+
### Uploading to a Storage Integration
|
|
347
|
+
|
|
348
|
+
`WherobotsJob` stays focused on job orchestration — it doesn't take any
|
|
349
|
+
"where files go" kwargs. To upload to a [Storage Integration](https://docs.wherobots.com/latest/develop/storage-management/storage/)
|
|
350
|
+
or a custom subdirectory, use `FilesAPI` directly and pass the resulting
|
|
351
|
+
`s3://` URI to `WherobotsJob`:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from wherobots import FilesAPI, WherobotsJob
|
|
355
|
+
from wherobots.config import WherobotsConfig
|
|
356
|
+
|
|
357
|
+
with FilesAPI.from_config(WherobotsConfig.from_env()) as files:
|
|
358
|
+
si = files.resolve_integration("customer-bucket")
|
|
359
|
+
# Build explicit destinations; upload_file is the "upload this file
|
|
360
|
+
# to this exact s3 path" primitive.
|
|
361
|
+
script_uri = files.upload_file(
|
|
362
|
+
"my_job.py",
|
|
363
|
+
files.dest_uri_for(si, "my_job.py", "job-scripts/sdk-e2e"),
|
|
364
|
+
)
|
|
365
|
+
wheel_uri = files.upload_file(
|
|
366
|
+
"dist/internal-lib-1.0.whl",
|
|
367
|
+
files.dest_uri_for(si, "internal-lib-1.0.whl", "job-scripts/sdk-e2e"),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
job = WherobotsJob(
|
|
371
|
+
script=script_uri, # already-uploaded URI
|
|
372
|
+
name="job-in-integration",
|
|
373
|
+
dependencies=[WherobotsJob.add_file_dependency(wheel_uri)],
|
|
374
|
+
)
|
|
375
|
+
job.submit()
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Both artifacts land under `s3://<integration.path>/job-scripts/sdk-e2e/`.
|
|
379
|
+
|
|
380
|
+
`FilesAPI` is also where to look for low-level controls (custom managed
|
|
381
|
+
subdirs, raw `s3://` destinations, STS credential fetch for BYO upload
|
|
382
|
+
tooling). See `FilesAPI.upload_file`, `upload_managed_file`,
|
|
383
|
+
`upload_dependency`, `dest_uri_for`, and `get_storage_credentials`.
|
|
384
|
+
|
|
310
385
|
#### JAR Jobs
|
|
311
386
|
|
|
312
387
|
```python
|
|
@@ -154,7 +154,7 @@ WherobotsJob(
|
|
|
154
154
|
spark_driver_disk_gb: int | None = None, # Driver disk size (GB)
|
|
155
155
|
spark_executor_disk_gb: int | None = None, # Executor disk size (GB)
|
|
156
156
|
jar_main_class: str | None = None, # Main class (required for JARs)
|
|
157
|
-
auto_upload: bool = True, # Upload local files to S3
|
|
157
|
+
auto_upload: bool = True, # Upload local files to S3 (managed storage)
|
|
158
158
|
base_url: str | None = None, # Override API URL
|
|
159
159
|
request_timeout_seconds: int | None = None, # HTTP timeout
|
|
160
160
|
config: WherobotsConfig | None = None, # Full config object
|
|
@@ -202,7 +202,7 @@ with WherobotsJob(script="s3://bucket/script.py", name="my-job") as job:
|
|
|
202
202
|
| `from_run_id(run_id, api_key=None, ...)` | `WherobotsJob` | Attach to an existing run for read-only log/metric access. No script required. `submit()` is disabled on the returned instance. |
|
|
203
203
|
| `list_runs(...)` | `RunListPage` | List runs with optional filters. No instance required. |
|
|
204
204
|
| `add_pypi_dependency(name, version)` | `dict` | Create a PyPI dependency dict for the `dependencies` parameter. |
|
|
205
|
-
| `add_file_dependency(file_path)` | `dict` | Create a file dependency dict (`.jar`, `.whl`, `.zip`, `.json`). |
|
|
205
|
+
| `add_file_dependency(file_path)` | `dict` | Create a file dependency dict (`.jar`, `.whl`, `.zip`, `.json`). Accepts an `s3://` URI or a local path — local paths are auto-uploaded to **managed storage** at `submit()` time. For non-managed destinations, upload via `FilesAPI` first and pass the resulting `s3://` URI. |
|
|
206
206
|
|
|
207
207
|
#### Attaching to an Existing Run
|
|
208
208
|
|
|
@@ -274,6 +274,80 @@ job = WherobotsJob(
|
|
|
274
274
|
)
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
+
##### Local Dependency Files
|
|
278
|
+
|
|
279
|
+
`add_file_dependency` also accepts a **local path** to a `.whl`, `.jar`, `.zip`,
|
|
280
|
+
or `.json`. The SDK uploads the file just before the job is submitted, fetching
|
|
281
|
+
short-lived AWS STS credentials from the Wherobots API (`POST /storage/credentials`)
|
|
282
|
+
and using boto3 multipart under the hood — no AWS credentials need to leave your
|
|
283
|
+
environment.
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
job = WherobotsJob(
|
|
287
|
+
script="my_job.py", # local script auto-upload still works
|
|
288
|
+
name="job-with-local-wheel",
|
|
289
|
+
dependencies=[
|
|
290
|
+
# Local wheel — uploaded to managed storage at submit() time
|
|
291
|
+
WherobotsJob.add_file_dependency("dist/mypkg-0.1.0-py3-none-any.whl"),
|
|
292
|
+
# Already-uploaded artifact — used as-is
|
|
293
|
+
WherobotsJob.add_file_dependency("s3://bucket/shared-libs/lib-2.0.jar"),
|
|
294
|
+
WherobotsJob.add_pypi_dependency("pandas", "2.0.3"),
|
|
295
|
+
],
|
|
296
|
+
)
|
|
297
|
+
job.submit()
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Auto-upload always targets the org's **managed storage**:
|
|
301
|
+
|
|
302
|
+
| Artifact | Destination |
|
|
303
|
+
|---|---|
|
|
304
|
+
| Script (when `script` is a local path) | `s3://<managed.path>/scripts/<filename>` |
|
|
305
|
+
| Dependency (when path is a local file) | `s3://<managed.path>/dependencies/<filename>` |
|
|
306
|
+
|
|
307
|
+
Local dependency uploads honor the same `auto_upload` flag as the script —
|
|
308
|
+
if `auto_upload=False`, attempting to submit a job with a local dependency
|
|
309
|
+
path raises `WherobotsValidationError`. The 5 GiB cap on a single upload
|
|
310
|
+
applies; for larger artifacts, pre-upload and pass an `s3://` URI.
|
|
311
|
+
|
|
312
|
+
### Uploading to a Storage Integration
|
|
313
|
+
|
|
314
|
+
`WherobotsJob` stays focused on job orchestration — it doesn't take any
|
|
315
|
+
"where files go" kwargs. To upload to a [Storage Integration](https://docs.wherobots.com/latest/develop/storage-management/storage/)
|
|
316
|
+
or a custom subdirectory, use `FilesAPI` directly and pass the resulting
|
|
317
|
+
`s3://` URI to `WherobotsJob`:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
from wherobots import FilesAPI, WherobotsJob
|
|
321
|
+
from wherobots.config import WherobotsConfig
|
|
322
|
+
|
|
323
|
+
with FilesAPI.from_config(WherobotsConfig.from_env()) as files:
|
|
324
|
+
si = files.resolve_integration("customer-bucket")
|
|
325
|
+
# Build explicit destinations; upload_file is the "upload this file
|
|
326
|
+
# to this exact s3 path" primitive.
|
|
327
|
+
script_uri = files.upload_file(
|
|
328
|
+
"my_job.py",
|
|
329
|
+
files.dest_uri_for(si, "my_job.py", "job-scripts/sdk-e2e"),
|
|
330
|
+
)
|
|
331
|
+
wheel_uri = files.upload_file(
|
|
332
|
+
"dist/internal-lib-1.0.whl",
|
|
333
|
+
files.dest_uri_for(si, "internal-lib-1.0.whl", "job-scripts/sdk-e2e"),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
job = WherobotsJob(
|
|
337
|
+
script=script_uri, # already-uploaded URI
|
|
338
|
+
name="job-in-integration",
|
|
339
|
+
dependencies=[WherobotsJob.add_file_dependency(wheel_uri)],
|
|
340
|
+
)
|
|
341
|
+
job.submit()
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Both artifacts land under `s3://<integration.path>/job-scripts/sdk-e2e/`.
|
|
345
|
+
|
|
346
|
+
`FilesAPI` is also where to look for low-level controls (custom managed
|
|
347
|
+
subdirs, raw `s3://` destinations, STS credential fetch for BYO upload
|
|
348
|
+
tooling). See `FilesAPI.upload_file`, `upload_managed_file`,
|
|
349
|
+
`upload_dependency`, `dest_uri_for`, and `get_storage_credentials`.
|
|
350
|
+
|
|
277
351
|
#### JAR Jobs
|
|
278
352
|
|
|
279
353
|
```python
|
|
@@ -26,6 +26,7 @@ classifiers = [
|
|
|
26
26
|
keywords = ["wherobots", "spark", "sedona", "geospatial", "jobs", "sdk", "rest-api"]
|
|
27
27
|
dependencies = [
|
|
28
28
|
"requests>=2.33.0",
|
|
29
|
+
"boto3>=1.34.0",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
[project.urls]
|
|
@@ -85,6 +86,13 @@ show_error_codes = true
|
|
|
85
86
|
module = "tests.*"
|
|
86
87
|
ignore_errors = true
|
|
87
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
|
|
95
|
+
|
|
88
96
|
[tool.pytest.ini_options]
|
|
89
97
|
testpaths = ["tests"]
|
|
90
98
|
python_files = ["test_*.py"]
|
|
@@ -329,7 +329,7 @@ class TestPresignedUpload:
|
|
|
329
329
|
def test_presigned_happy_path(self, mock_from_config, mock_env):
|
|
330
330
|
"""Presigned upload succeeds via _prepare_script_uri()."""
|
|
331
331
|
mock_files_api = MagicMock()
|
|
332
|
-
mock_files_api.
|
|
332
|
+
mock_files_api.upload_managed_file.return_value = (
|
|
333
333
|
"s3://managed-bucket/org/data/shared/scripts/my_script.py"
|
|
334
334
|
)
|
|
335
335
|
mock_from_config.return_value = mock_files_api
|
|
@@ -342,14 +342,14 @@ class TestPresignedUpload:
|
|
|
342
342
|
uri = job._prepare_script_uri()
|
|
343
343
|
|
|
344
344
|
assert uri == "s3://managed-bucket/org/data/shared/scripts/my_script.py"
|
|
345
|
-
mock_files_api.
|
|
345
|
+
mock_files_api.upload_managed_file.assert_called_once_with("my_script.py", subdir="scripts")
|
|
346
346
|
mock_files_api.close.assert_called_once()
|
|
347
347
|
|
|
348
348
|
@patch("wherobots.api.files.FilesAPI.from_config")
|
|
349
349
|
def test_presigned_success_caches_uri(self, mock_from_config, mock_env):
|
|
350
350
|
"""_prepare_script_uri() caches the result and doesn't re-upload."""
|
|
351
351
|
mock_files_api = MagicMock()
|
|
352
|
-
mock_files_api.
|
|
352
|
+
mock_files_api.upload_managed_file.return_value = (
|
|
353
353
|
"s3://managed-bucket/org/data/shared/scripts/my_script.py"
|
|
354
354
|
)
|
|
355
355
|
mock_from_config.return_value = mock_files_api
|
|
@@ -363,14 +363,16 @@ class TestPresignedUpload:
|
|
|
363
363
|
uri2 = job._prepare_script_uri()
|
|
364
364
|
|
|
365
365
|
assert uri1 == uri2
|
|
366
|
-
#
|
|
367
|
-
mock_files_api.
|
|
366
|
+
# upload_managed_file should only be called once (cached on second call)
|
|
367
|
+
mock_files_api.upload_managed_file.assert_called_once()
|
|
368
368
|
|
|
369
369
|
@patch("wherobots.api.files.FilesAPI.from_config")
|
|
370
370
|
def test_presigned_fails_raises_directly(self, mock_from_config, mock_env):
|
|
371
371
|
"""Presigned upload fails → WherobotsAPIError raised with no fallback."""
|
|
372
372
|
mock_files_api = MagicMock()
|
|
373
|
-
mock_files_api.
|
|
373
|
+
mock_files_api.upload_managed_file.side_effect = WherobotsAPIError(
|
|
374
|
+
"presigned 500", status_code=500
|
|
375
|
+
)
|
|
374
376
|
mock_from_config.return_value = mock_files_api
|
|
375
377
|
|
|
376
378
|
job = WherobotsJob(
|
|
@@ -387,7 +389,7 @@ class TestPresignedUpload:
|
|
|
387
389
|
def test_presigned_closes_files_api_on_success(self, mock_from_config, mock_env):
|
|
388
390
|
"""FilesAPI.close() is called even when upload succeeds."""
|
|
389
391
|
mock_files_api = MagicMock()
|
|
390
|
-
mock_files_api.
|
|
392
|
+
mock_files_api.upload_managed_file.return_value = "s3://bucket/key"
|
|
391
393
|
mock_from_config.return_value = mock_files_api
|
|
392
394
|
|
|
393
395
|
job = WherobotsJob(
|
|
@@ -403,7 +405,7 @@ class TestPresignedUpload:
|
|
|
403
405
|
def test_presigned_closes_files_api_on_failure(self, mock_from_config, mock_env):
|
|
404
406
|
"""FilesAPI.close() is called even when upload fails."""
|
|
405
407
|
mock_files_api = MagicMock()
|
|
406
|
-
mock_files_api.
|
|
408
|
+
mock_files_api.upload_managed_file.side_effect = WherobotsAPIError("boom")
|
|
407
409
|
mock_from_config.return_value = mock_files_api
|
|
408
410
|
|
|
409
411
|
job = WherobotsJob(
|
|
@@ -665,3 +667,157 @@ class TestPollForLogsDrain:
|
|
|
665
667
|
job.poll_for_logs(follow=False, log_handler=captured.append)
|
|
666
668
|
|
|
667
669
|
assert job._api.get_logs.call_count == 1
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
# ── add_file_dependency local-path / lazy upload ─────────────────────────
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class TestAddFileDependency:
|
|
676
|
+
def test_s3_uri_unchanged(self):
|
|
677
|
+
dep = WherobotsJob.add_file_dependency("s3://bucket/lib.whl")
|
|
678
|
+
assert dep == {"sourceType": "FILE", "filePath": "s3://bucket/lib.whl"}
|
|
679
|
+
assert "_pendingUpload" not in dep
|
|
680
|
+
|
|
681
|
+
def test_local_path_marks_pending(self):
|
|
682
|
+
dep = WherobotsJob.add_file_dependency("dist/lib.whl")
|
|
683
|
+
assert dep["filePath"] == "dist/lib.whl"
|
|
684
|
+
assert dep["_pendingUpload"] is True
|
|
685
|
+
# Storage-integration routing belongs to FilesAPI now — sentinel
|
|
686
|
+
# keys for SI / dest_subdir are not present.
|
|
687
|
+
assert "_storageIntegration" not in dep
|
|
688
|
+
assert "_destSubdir" not in dep
|
|
689
|
+
|
|
690
|
+
def test_bad_extension_raises(self):
|
|
691
|
+
with pytest.raises(WherobotsValidationError, match="extensions"):
|
|
692
|
+
WherobotsJob.add_file_dependency("dist/lib.tar.gz")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
class TestPrepareDependencies:
|
|
696
|
+
@patch("wherobots.api.files.FilesAPI.from_config")
|
|
697
|
+
def test_local_dep_is_uploaded_and_sentinels_stripped(self, mock_from_config, mock_env):
|
|
698
|
+
mock_files = MagicMock()
|
|
699
|
+
mock_files.upload_dependency.return_value = "s3://managed/org/dependencies/lib.whl"
|
|
700
|
+
mock_from_config.return_value = mock_files
|
|
701
|
+
|
|
702
|
+
job = WherobotsJob(
|
|
703
|
+
script="s3://bucket/main.py",
|
|
704
|
+
name="local-dep-job",
|
|
705
|
+
runtime="tiny",
|
|
706
|
+
dependencies=[
|
|
707
|
+
WherobotsJob.add_file_dependency("dist/lib.whl"),
|
|
708
|
+
WherobotsJob.add_pypi_dependency("pandas", "2.0.3"),
|
|
709
|
+
],
|
|
710
|
+
)
|
|
711
|
+
job._prepare_dependencies()
|
|
712
|
+
|
|
713
|
+
# First dep resolved + sentinel keys gone.
|
|
714
|
+
local_dep = job.dependencies[0]
|
|
715
|
+
assert local_dep["filePath"] == "s3://managed/org/dependencies/lib.whl"
|
|
716
|
+
assert "_pendingUpload" not in local_dep
|
|
717
|
+
assert "_storageIntegration" not in local_dep
|
|
718
|
+
# PyPI dep untouched.
|
|
719
|
+
assert job.dependencies[1] == {
|
|
720
|
+
"sourceType": "PYPI",
|
|
721
|
+
"libraryName": "pandas",
|
|
722
|
+
"libraryVersion": "2.0.3",
|
|
723
|
+
}
|
|
724
|
+
mock_files.upload_dependency.assert_called_once_with("dist/lib.whl")
|
|
725
|
+
mock_files.close.assert_called_once()
|
|
726
|
+
|
|
727
|
+
def test_no_pending_deps_skips_files_api(self, mock_env):
|
|
728
|
+
"""If everything is already s3://, no FilesAPI session is created."""
|
|
729
|
+
with patch("wherobots.api.files.FilesAPI.from_config") as mock_from_config:
|
|
730
|
+
job = WherobotsJob(
|
|
731
|
+
script="s3://bucket/main.py",
|
|
732
|
+
name="all-s3-deps",
|
|
733
|
+
runtime="tiny",
|
|
734
|
+
dependencies=[
|
|
735
|
+
WherobotsJob.add_file_dependency("s3://b/a.whl"),
|
|
736
|
+
WherobotsJob.add_pypi_dependency("x", "1"),
|
|
737
|
+
],
|
|
738
|
+
)
|
|
739
|
+
job._prepare_dependencies()
|
|
740
|
+
mock_from_config.assert_not_called()
|
|
741
|
+
|
|
742
|
+
@patch("wherobots.api.files.FilesAPI.from_config")
|
|
743
|
+
def test_partial_failure_leaves_dependencies_untouched(self, mock_from_config, mock_env):
|
|
744
|
+
"""If upload_dependency raises mid-list, ``self.dependencies``
|
|
745
|
+
stays in its original sentinel-tagged form so a retry runs the
|
|
746
|
+
same work cleanly. No partially-resolved state ships."""
|
|
747
|
+
mock_files = MagicMock()
|
|
748
|
+
mock_files.upload_dependency.side_effect = [
|
|
749
|
+
"s3://managed/dependencies/first.whl",
|
|
750
|
+
WherobotsAPIError("upload boom"),
|
|
751
|
+
]
|
|
752
|
+
mock_from_config.return_value = mock_files
|
|
753
|
+
|
|
754
|
+
job = WherobotsJob(
|
|
755
|
+
script="s3://bucket/main.py",
|
|
756
|
+
name="partial-failure-job",
|
|
757
|
+
runtime="tiny",
|
|
758
|
+
dependencies=[
|
|
759
|
+
WherobotsJob.add_file_dependency("dist/first.whl"),
|
|
760
|
+
WherobotsJob.add_file_dependency("dist/second.whl"),
|
|
761
|
+
],
|
|
762
|
+
)
|
|
763
|
+
before = [dict(d) for d in job.dependencies]
|
|
764
|
+
|
|
765
|
+
with pytest.raises(WherobotsAPIError, match="upload boom"):
|
|
766
|
+
job._prepare_dependencies()
|
|
767
|
+
|
|
768
|
+
# Both sentinel-tagged dicts still untouched on the instance.
|
|
769
|
+
assert job.dependencies == before
|
|
770
|
+
assert all(d.get("_pendingUpload") is True for d in job.dependencies)
|
|
771
|
+
|
|
772
|
+
def test_auto_upload_false_raises_on_local_dep(self, mock_env):
|
|
773
|
+
job = WherobotsJob(
|
|
774
|
+
script="s3://bucket/main.py",
|
|
775
|
+
name="no-auto-upload",
|
|
776
|
+
runtime="tiny",
|
|
777
|
+
auto_upload=False,
|
|
778
|
+
dependencies=[WherobotsJob.add_file_dependency("dist/lib.whl")],
|
|
779
|
+
)
|
|
780
|
+
with pytest.raises(WherobotsValidationError, match="auto_upload"):
|
|
781
|
+
job._prepare_dependencies()
|
|
782
|
+
|
|
783
|
+
@patch("wherobots.api.files.FilesAPI.from_config")
|
|
784
|
+
def test_build_payload_runs_dependency_prep(self, mock_from_config, mock_env):
|
|
785
|
+
"""_build_payload must call _prepare_dependencies before serializing."""
|
|
786
|
+
mock_files = MagicMock()
|
|
787
|
+
mock_files.upload_dependency.return_value = "s3://managed/dependencies/lib.whl"
|
|
788
|
+
mock_from_config.return_value = mock_files
|
|
789
|
+
|
|
790
|
+
job = WherobotsJob(
|
|
791
|
+
script="s3://bucket/main.py",
|
|
792
|
+
name="build-payload-job",
|
|
793
|
+
runtime="tiny",
|
|
794
|
+
dependencies=[WherobotsJob.add_file_dependency("dist/lib.whl")],
|
|
795
|
+
)
|
|
796
|
+
payload = job._build_payload()
|
|
797
|
+
|
|
798
|
+
# The CreateRunPayload must serialize cleanly — no sentinel keys leak.
|
|
799
|
+
body = payload.to_dict()
|
|
800
|
+
env = body["environment"]
|
|
801
|
+
assert env["dependencies"] == [
|
|
802
|
+
{"sourceType": "FILE", "filePath": "s3://managed/dependencies/lib.whl"}
|
|
803
|
+
]
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
# ── Script auto-upload stays scoped to managed storage ────────────────
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
class TestScriptAutoUpload:
|
|
810
|
+
@patch("wherobots.api.files.FilesAPI.from_config")
|
|
811
|
+
def test_script_auto_upload_routes_to_managed(self, mock_from_config, mock_env):
|
|
812
|
+
"""``WherobotsJob`` auto-upload always targets managed storage.
|
|
813
|
+
Integration uploads are an explicit ``FilesAPI`` step."""
|
|
814
|
+
mock_files = MagicMock()
|
|
815
|
+
mock_files.upload_managed_file.return_value = "s3://managed/scripts/job.py"
|
|
816
|
+
mock_from_config.return_value = mock_files
|
|
817
|
+
|
|
818
|
+
job = WherobotsJob(script="job.py", name="default-script", runtime="tiny")
|
|
819
|
+
uri = job._prepare_script_uri()
|
|
820
|
+
|
|
821
|
+
assert uri == "s3://managed/scripts/job.py"
|
|
822
|
+
mock_files.upload_managed_file.assert_called_once_with("job.py", subdir="scripts")
|
|
823
|
+
mock_files.upload_dependency.assert_not_called()
|