aind-codeocean-dispatch 0.2.0__tar.gz → 0.2.2__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.
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/CHANGELOG.md +12 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/PKG-INFO +2 -2
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/pyproject.toml +2 -2
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/src/aind_codeocean_dispatch/runner.py +30 -9
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/tests/fakes.py +19 -7
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/tests/test_runner.py +34 -2
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/uv.lock +2 -2
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.copier-answers.yml +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/ISSUE_TEMPLATE/user-story.md +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/dependabot.yml +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/workflows/bump-call.yml +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/workflows/ci-call.yml +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/workflows/publish-call.yml +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.gitignore +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.python-version +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/CLAUDE.md +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/LICENSE +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/README.md +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/scripts/run_linters_and_checks.sh +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/src/aind_codeocean_dispatch/__init__.py +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/src/aind_codeocean_dispatch/py.typed +0 -0
- {aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/tests/__init__.py +0 -0
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## v0.2.2 (2026-10-15)
|
|
9
|
+
|
|
10
|
+
### Fix
|
|
11
|
+
|
|
12
|
+
- run on a non-retrying copy of the caller's client
|
|
13
|
+
|
|
14
|
+
## v0.2.1 (2026-10-15)
|
|
15
|
+
|
|
16
|
+
### Fix
|
|
17
|
+
|
|
18
|
+
- support codeocean 0.13 and reuse only the caller's own captures
|
|
19
|
+
|
|
8
20
|
## v0.2.0 (2026-10-15)
|
|
9
21
|
|
|
10
22
|
### Feat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: aind-codeocean-dispatch
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Launch, monitor, and capture batches of Code Ocean capsule runs
|
|
5
5
|
Project-URL: Repository, https://github.com/AllenNeuralDynamics/aind-codeocean-dispatch/
|
|
6
6
|
Project-URL: Issues, https://github.com/AllenNeuralDynamics/aind-codeocean-dispatch/issues
|
|
@@ -12,7 +12,7 @@ License-File: LICENSE
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
14
|
Requires-Dist: aind-code-ocean-pipeline-utils>=0.7.1
|
|
15
|
-
Requires-Dist: codeocean>=0.
|
|
15
|
+
Requires-Dist: codeocean>=0.13
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
18
18
|
# AIND Code Ocean dispatch
|
|
@@ -13,11 +13,11 @@ authors = [
|
|
|
13
13
|
]
|
|
14
14
|
classifiers = ["Programming Language :: Python :: 3"]
|
|
15
15
|
readme = "README.md"
|
|
16
|
-
version = "0.2.
|
|
16
|
+
version = "0.2.2"
|
|
17
17
|
|
|
18
18
|
dependencies = [
|
|
19
19
|
"aind-code-ocean-pipeline-utils>=0.7.1",
|
|
20
|
-
"codeocean>=0.
|
|
20
|
+
"codeocean>=0.13",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[project.urls]
|
|
@@ -6,7 +6,9 @@ counters rather than locks, and no thread is parked per job.
|
|
|
6
6
|
|
|
7
7
|
Error policy
|
|
8
8
|
------------
|
|
9
|
-
|
|
9
|
+
Every 429 or 5xx must surface here, so :func:`run_jobs` works on a copy of the
|
|
10
|
+
client with ``retries=0``: transport-level retries would resend POSTs below this
|
|
11
|
+
policy, and their exhausted retries raise without the status code it reads.
|
|
10
12
|
|
|
11
13
|
- **429** is retried on every call, honouring ``Retry-After``: the server rejected
|
|
12
14
|
the request without acting on it.
|
|
@@ -33,9 +35,13 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
|
33
35
|
import requests
|
|
34
36
|
from aind_code_ocean_pipeline_utils.cache import input_fingerprint
|
|
35
37
|
from aind_code_ocean_pipeline_utils.process import GracefulExit, check_shutdown
|
|
38
|
+
|
|
39
|
+
# Import paths that exist across supported SDK versions; 0.17 moved the models to
|
|
40
|
+
# ``codeocean.models`` and kept these as re-exports.
|
|
41
|
+
from codeocean.components import Ownership
|
|
42
|
+
from codeocean.computation import Computation, ComputationEndStatus, ComputationState, RunParams
|
|
43
|
+
from codeocean.data_asset import DataAsset, DataAssetParams, DataAssetSearchParams, DataAssetState
|
|
36
44
|
from codeocean.error import Error as CodeOceanError
|
|
37
|
-
from codeocean.models.computation import Computation, ComputationEndStatus, ComputationState, RunParams
|
|
38
|
-
from codeocean.models.data_asset import DataAsset, DataAssetParams, DataAssetSearchParams, DataAssetState
|
|
39
45
|
|
|
40
46
|
if TYPE_CHECKING:
|
|
41
47
|
from codeocean import CodeOcean
|
|
@@ -88,8 +94,8 @@ class Job:
|
|
|
88
94
|
can depend on it. ``None`` stops at a successful computation.
|
|
89
95
|
key : str, optional
|
|
90
96
|
Resume key (see :func:`job_key`), added to the captured asset as
|
|
91
|
-
:func:`key_tag`. With ``reuse=True``, a Ready asset carrying it
|
|
92
|
-
instead of launching. ``None`` never reuses.
|
|
97
|
+
:func:`key_tag`. With ``reuse=True``, a Ready asset carrying it that the
|
|
98
|
+
caller created is returned instead of launching. ``None`` never reuses.
|
|
93
99
|
"""
|
|
94
100
|
|
|
95
101
|
label: str
|
|
@@ -228,7 +234,8 @@ def run_jobs(
|
|
|
228
234
|
Parameters
|
|
229
235
|
----------
|
|
230
236
|
client : CodeOcean
|
|
231
|
-
Authenticated client.
|
|
237
|
+
Authenticated client. Its own ``retries`` setting is overridden on a copy;
|
|
238
|
+
the caller's client is left unchanged.
|
|
232
239
|
jobs : Sequence[Job]
|
|
233
240
|
Launched in order, subject to the cap and pacing.
|
|
234
241
|
max_in_flight : int, optional
|
|
@@ -243,7 +250,9 @@ def run_jobs(
|
|
|
243
250
|
capture_retries : int, optional
|
|
244
251
|
New capture attempts per job after a capture ends in the Failed state.
|
|
245
252
|
reuse : bool, optional
|
|
246
|
-
Return a Ready asset tagged with a job's key instead of launching it.
|
|
253
|
+
Return a Ready asset tagged with a job's key instead of launching it. Only
|
|
254
|
+
assets the caller created count: another user running identical inputs
|
|
255
|
+
produces the same key, and their asset is not the caller's to consume.
|
|
247
256
|
poll_interval_s : float, optional
|
|
248
257
|
Seconds between status checks of each running computation.
|
|
249
258
|
timeout_s : float, optional
|
|
@@ -265,7 +274,7 @@ def run_jobs(
|
|
|
265
274
|
"""
|
|
266
275
|
_validate(jobs, max_in_flight=max_in_flight, start_retries=start_retries, capture_retries=capture_retries)
|
|
267
276
|
return _Dispatcher(
|
|
268
|
-
client,
|
|
277
|
+
_without_transport_retries(client),
|
|
269
278
|
jobs,
|
|
270
279
|
max_in_flight=max_in_flight,
|
|
271
280
|
launch_interval_s=launch_interval_s,
|
|
@@ -278,6 +287,18 @@ def run_jobs(
|
|
|
278
287
|
).run()
|
|
279
288
|
|
|
280
289
|
|
|
290
|
+
def _without_transport_retries(client: CodeOcean) -> CodeOcean:
|
|
291
|
+
"""Return *client*, or a copy with ``retries=0`` if it retries requests itself.
|
|
292
|
+
|
|
293
|
+
The copy opens a new session, so anything mounted on the original's ``session``
|
|
294
|
+
by hand is not carried over.
|
|
295
|
+
"""
|
|
296
|
+
if client.retries in (0, None):
|
|
297
|
+
return client
|
|
298
|
+
logger.info("using a copy of the client with retries=0 (was %r); run_jobs retries what is safe", client.retries)
|
|
299
|
+
return dataclasses.replace(client, retries=0)
|
|
300
|
+
|
|
301
|
+
|
|
281
302
|
def _validate(jobs: Sequence[Job], *, max_in_flight: int | None, start_retries: int, capture_retries: int) -> None:
|
|
282
303
|
labels = [job.label for job in jobs]
|
|
283
304
|
if len(set(labels)) != len(labels):
|
|
@@ -479,7 +500,7 @@ class _Dispatcher:
|
|
|
479
500
|
if slot.next_at > _now():
|
|
480
501
|
continue
|
|
481
502
|
tag = key_tag(slot.job.key)
|
|
482
|
-
params = DataAssetSearchParams(query=f"tag:{tag}", limit=100)
|
|
503
|
+
params = DataAssetSearchParams(query=f"tag:{tag}", ownership=Ownership.Created, limit=100)
|
|
483
504
|
try:
|
|
484
505
|
results = self.client.data_assets.search_data_assets(params)
|
|
485
506
|
except (CodeOceanError, requests.RequestException) as exc:
|
|
@@ -8,14 +8,14 @@ creation order.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import dataclasses
|
|
11
12
|
import itertools
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
15
16
|
import requests
|
|
16
|
-
from codeocean.
|
|
17
|
-
from codeocean.
|
|
18
|
-
from codeocean.models.data_asset import (
|
|
17
|
+
from codeocean.computation import Computation, ComputationEndStatus, ComputationState, RunParams
|
|
18
|
+
from codeocean.data_asset import (
|
|
19
19
|
DataAsset,
|
|
20
20
|
DataAssetParams,
|
|
21
21
|
DataAssetSearchParams,
|
|
@@ -23,6 +23,16 @@ from codeocean.models.data_asset import (
|
|
|
23
23
|
DataAssetState,
|
|
24
24
|
DataAssetType,
|
|
25
25
|
)
|
|
26
|
+
from codeocean.error import Error as CodeOceanError
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build(cls: Any, **values: Any) -> Any:
|
|
30
|
+
"""Construct an SDK model, dropping fields this SDK version does not declare.
|
|
31
|
+
|
|
32
|
+
Required fields differ across supported versions (0.17 added ``owner``).
|
|
33
|
+
"""
|
|
34
|
+
declared = {f.name for f in dataclasses.fields(cls)}
|
|
35
|
+
return cls(**{name: value for name, value in values.items() if name in declared})
|
|
26
36
|
|
|
27
37
|
|
|
28
38
|
class FakeClock:
|
|
@@ -69,7 +79,8 @@ CRASHED = snapshot(ComputationState.Completed, end_status=ComputationEndStatus.F
|
|
|
69
79
|
|
|
70
80
|
|
|
71
81
|
def asset(asset_id: str, state: DataAssetState, *, tags: list[str] | None = None, created: int = 0) -> DataAsset:
|
|
72
|
-
return
|
|
82
|
+
return build(
|
|
83
|
+
DataAsset,
|
|
73
84
|
id=asset_id,
|
|
74
85
|
created=created,
|
|
75
86
|
name=asset_id,
|
|
@@ -115,7 +126,7 @@ class FakeComputations:
|
|
|
115
126
|
|
|
116
127
|
@staticmethod
|
|
117
128
|
def _computation(computation_id: str, step: dict[str, Any]) -> Computation:
|
|
118
|
-
return Computation
|
|
129
|
+
return build(Computation, id=computation_id, created=0, name=computation_id, owner="owner", run_time=0, **step)
|
|
119
130
|
|
|
120
131
|
|
|
121
132
|
@dataclass
|
|
@@ -125,7 +136,7 @@ class FakeDataAssets:
|
|
|
125
136
|
existing: list[DataAsset] = field(default_factory=list)
|
|
126
137
|
search_errors: list[Exception] = field(default_factory=list)
|
|
127
138
|
created: list[DataAssetParams] = field(default_factory=list)
|
|
128
|
-
searches: list[
|
|
139
|
+
searches: list[DataAssetSearchParams] = field(default_factory=list)
|
|
129
140
|
_scripts: dict[str, list[Any]] = field(default_factory=dict)
|
|
130
141
|
|
|
131
142
|
def create_data_asset(self, data_asset_params: DataAssetParams) -> DataAsset:
|
|
@@ -145,7 +156,7 @@ class FakeDataAssets:
|
|
|
145
156
|
return asset(data_asset_id, step)
|
|
146
157
|
|
|
147
158
|
def search_data_assets(self, search_params: DataAssetSearchParams) -> DataAssetSearchResults:
|
|
148
|
-
self.searches.append(search_params
|
|
159
|
+
self.searches.append(search_params)
|
|
149
160
|
if self.search_errors:
|
|
150
161
|
raise self.search_errors.pop(0)
|
|
151
162
|
return DataAssetSearchResults(has_more=False, results=list(self.existing))
|
|
@@ -155,3 +166,4 @@ class FakeDataAssets:
|
|
|
155
166
|
class FakeClient:
|
|
156
167
|
computations: FakeComputations
|
|
157
168
|
data_assets: FakeDataAssets
|
|
169
|
+
retries: Any = 0
|
|
@@ -5,8 +5,11 @@ from typing import Any
|
|
|
5
5
|
import pytest
|
|
6
6
|
import requests
|
|
7
7
|
from aind_code_ocean_pipeline_utils.process import GracefulExit
|
|
8
|
-
from codeocean
|
|
9
|
-
from codeocean.
|
|
8
|
+
from codeocean import CodeOcean
|
|
9
|
+
from codeocean.components import Ownership
|
|
10
|
+
from codeocean.computation import Computation, DataAssetsRunParam, NamedRunParam, RunParams
|
|
11
|
+
from codeocean.data_asset import DataAssetParams, DataAssetState
|
|
12
|
+
from urllib3.util import Retry
|
|
10
13
|
|
|
11
14
|
from aind_codeocean_dispatch import (
|
|
12
15
|
DispatchFailed,
|
|
@@ -255,6 +258,8 @@ def test_reuse_returns_newest_ready_asset_with_exact_tag(clock: FakeClock) -> No
|
|
|
255
258
|
assert outcome.status == "reused"
|
|
256
259
|
assert outcome.asset_id == "new"
|
|
257
260
|
assert client.computations.launches == []
|
|
261
|
+
(search,) = client.data_assets.searches
|
|
262
|
+
assert search.ownership == Ownership.Created
|
|
258
263
|
|
|
259
264
|
|
|
260
265
|
def test_reuse_launches_when_nothing_matches(clock: FakeClock) -> None:
|
|
@@ -312,6 +317,33 @@ def test_duplicate_labels_are_rejected(clock: FakeClock) -> None:
|
|
|
312
317
|
run_jobs(make_client(clock, {}), [job("a"), job("a")])
|
|
313
318
|
|
|
314
319
|
|
|
320
|
+
@pytest.mark.parametrize("retries", [3, Retry(total=5, allowed_methods=["POST"])])
|
|
321
|
+
def test_client_that_retries_itself_still_runs(clock: FakeClock, retries: Any) -> None:
|
|
322
|
+
client = make_client(clock, {"a": [[SUCCEEDED]]})
|
|
323
|
+
client.retries = retries
|
|
324
|
+
|
|
325
|
+
outcomes = run_jobs(client, [job("a")])
|
|
326
|
+
|
|
327
|
+
assert [o.status for o in outcomes] == ["captured"]
|
|
328
|
+
assert client.retries is retries
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_retrying_client_is_copied_without_retries() -> None:
|
|
332
|
+
original = CodeOcean(domain="https://codeocean.example", token="x", retries=Retry(total=5))
|
|
333
|
+
|
|
334
|
+
copy = runner._without_transport_retries(original)
|
|
335
|
+
|
|
336
|
+
assert copy is not original and copy.session is not original.session
|
|
337
|
+
assert (copy.retries, copy.domain, copy.token) == (0, original.domain, original.token)
|
|
338
|
+
assert isinstance(original.retries, Retry)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_non_retrying_client_is_used_as_is() -> None:
|
|
342
|
+
client = CodeOcean(domain="https://codeocean.example", token="x")
|
|
343
|
+
|
|
344
|
+
assert runner._without_transport_retries(client) is client
|
|
345
|
+
|
|
346
|
+
|
|
315
347
|
def test_invalid_key_is_rejected() -> None:
|
|
316
348
|
with pytest.raises(ValueError, match="key"):
|
|
317
349
|
job("a", key="has space")
|
|
@@ -17,7 +17,7 @@ wheels = [
|
|
|
17
17
|
|
|
18
18
|
[[package]]
|
|
19
19
|
name = "aind-codeocean-dispatch"
|
|
20
|
-
version = "0.2.
|
|
20
|
+
version = "0.2.2"
|
|
21
21
|
source = { editable = "." }
|
|
22
22
|
dependencies = [
|
|
23
23
|
{ name = "aind-code-ocean-pipeline-utils" },
|
|
@@ -50,7 +50,7 @@ types = [
|
|
|
50
50
|
[package.metadata]
|
|
51
51
|
requires-dist = [
|
|
52
52
|
{ name = "aind-code-ocean-pipeline-utils", specifier = ">=0.7.1" },
|
|
53
|
-
{ name = "codeocean", specifier = ">=0.
|
|
53
|
+
{ name = "codeocean", specifier = ">=0.13" },
|
|
54
54
|
]
|
|
55
55
|
|
|
56
56
|
[package.metadata.requires-dev]
|
|
File without changes
|
{aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.md
RENAMED
|
File without changes
|
|
File without changes
|
{aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/ISSUE_TEMPLATE/user-story.md
RENAMED
|
File without changes
|
|
File without changes
|
{aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/workflows/bump-call.yml
RENAMED
|
File without changes
|
{aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/workflows/ci-call.yml
RENAMED
|
File without changes
|
{aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/.github/workflows/publish-call.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/scripts/run_linters_and_checks.sh
RENAMED
|
File without changes
|
|
File without changes
|
{aind_codeocean_dispatch-0.2.0 → aind_codeocean_dispatch-0.2.2}/src/aind_codeocean_dispatch/py.typed
RENAMED
|
File without changes
|
|
File without changes
|