wt-runner 0.3.2__tar.gz → 0.3.3__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.
- {wt_runner-0.3.2 → wt_runner-0.3.3}/CHANGELOG.md +5 -0
- {wt_runner-0.3.2/src/wt_runner.egg-info → wt_runner-0.3.3}/PKG-INFO +1 -1
- {wt_runner-0.3.2 → wt_runner-0.3.3}/pyproject.toml +1 -1
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner/_version.py +3 -3
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner/app.py +31 -1
- {wt_runner-0.3.2 → wt_runner-0.3.3/src/wt_runner.egg-info}/PKG-INFO +1 -1
- wt_runner-0.3.3/src/wt_runner.egg-info/scm_version.json +8 -0
- wt_runner-0.3.2/src/wt_runner.egg-info/scm_version.json +0 -8
- {wt_runner-0.3.2 → wt_runner-0.3.3}/README.md +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/setup.cfg +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner/__init__.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner/py.typed +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner/testing.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner/tracing.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner.egg-info/SOURCES.txt +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner.egg-info/dependency_links.txt +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner.egg-info/requires.txt +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner.egg-info/scm_file_list.json +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/src/wt_runner.egg-info/top_level.txt +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/tests/__init__.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/tests/conftest.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/tests/test_app.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/tests/test_gcp_imports.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/tests/test_testing.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/tests/test_tracing.py +0 -0
- {wt_runner-0.3.2 → wt_runner-0.3.3}/uv.lock +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.3 — 2026-11-09
|
|
4
|
+
|
|
5
|
+
- `/run-from-pubsub` skips Pub/Sub redeliveries of a run that is already claimed or finished, via a create-only `lock.json` claim in the results location. A claim is never released, so a run whose attempt died is not retried ([#290](https://github.com/wildlife-dynamics/wt/pull/290))
|
|
6
|
+
- Fix the Pub/Sub wait timeout so it is actually capped at `PUBSUB_ACK_MAX_TIMEOUT` (570s); a larger requested timeout previously passed through unchanged ([#290](https://github.com/wildlife-dynamics/wt/pull/290))
|
|
7
|
+
|
|
3
8
|
## v0.3.2 — 2026-11-03
|
|
4
9
|
|
|
5
10
|
- Release to pick up the new `wt-invokers-gcp>=0.7.0` floor in `wt-runner-gcp`; no changes to the Python package contents
|
|
@@ -169,7 +169,7 @@ extend-immutable-calls = [
|
|
|
169
169
|
|
|
170
170
|
[tool.pixi.package]
|
|
171
171
|
name = "wt-runner"
|
|
172
|
-
version = "0.3.
|
|
172
|
+
version = "0.3.3"
|
|
173
173
|
|
|
174
174
|
[tool.pixi.package.build]
|
|
175
175
|
backend = { name = "pixi-build-python", version = "*", channels = ["conda-forge", "https://prefix.dev/pixi-build-backends"] }
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.3.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 3,
|
|
21
|
+
__version__ = version = '0.3.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 3)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g43bf668'
|
|
@@ -77,6 +77,7 @@ except ImportError:
|
|
|
77
77
|
ecoscope_get_results_json = None
|
|
78
78
|
|
|
79
79
|
import obstore
|
|
80
|
+
from obstore.exceptions import AlreadyExistsError
|
|
80
81
|
|
|
81
82
|
# Invoker registry mapping invoker names to classes
|
|
82
83
|
INVOKERS: dict[str, type[AbstractInvoker]] = {
|
|
@@ -92,6 +93,7 @@ TIMEOUT_EXPIRED_ERROR_MSG = (
|
|
|
92
93
|
"The workflow timed out. Consider reducing the amount of data being processed."
|
|
93
94
|
)
|
|
94
95
|
PUBSUB_ACK_MAX_TIMEOUT = 570 # seconds
|
|
96
|
+
RUN_LOCK_FILENAME = "lock.json"
|
|
95
97
|
|
|
96
98
|
|
|
97
99
|
async def get_results_json(results_url: str) -> dict[str, Any]:
|
|
@@ -484,6 +486,12 @@ async def run_from_pubsub(
|
|
|
484
486
|
"run-from-pubsub-endpoint",
|
|
485
487
|
attributes=asdict(span_attributes),
|
|
486
488
|
):
|
|
489
|
+
if not await claim_run(invoker_params["results_url"]):
|
|
490
|
+
logging.info( # noqa: LOG015 # routed through root logger by design
|
|
491
|
+
"Skipping redelivered run %s: it already has a claim or a result.",
|
|
492
|
+
span_attributes.workflow_run_id,
|
|
493
|
+
)
|
|
494
|
+
return {"status": "skipped"}
|
|
487
495
|
trace_context = build_context_headers()
|
|
488
496
|
invoker_params["extra_env"] |= {k.upper(): v for k, v in trace_context.items()}
|
|
489
497
|
invoker_params["otel_exporter"] = get_otel_exporter()
|
|
@@ -507,7 +515,7 @@ async def run_from_pubsub(
|
|
|
507
515
|
timeout = command_payload.invoker_kwargs.get("timeout", PUBSUB_ACK_MAX_TIMEOUT)
|
|
508
516
|
# Maximum timeout when running from PubSub is 10 minutes.
|
|
509
517
|
# It's set to a little bit less to have time to cancel and handle the error
|
|
510
|
-
timeout = min(timeout,
|
|
518
|
+
timeout = min(timeout, PUBSUB_ACK_MAX_TIMEOUT)
|
|
511
519
|
exit_code = await invoker.wait(timeout=timeout, error_msg=TIMEOUT_EXPIRED_ERROR_MSG)
|
|
512
520
|
if exit_code != 0:
|
|
513
521
|
raise RuntimeError(f"Workflow invoker failed with exit code {exit_code}.")
|
|
@@ -590,6 +598,28 @@ def prepare_invoker_parameters(
|
|
|
590
598
|
) # Extra kwargs are passed to the invoker
|
|
591
599
|
|
|
592
600
|
|
|
601
|
+
async def claim_run(results_url: str) -> bool:
|
|
602
|
+
"""Claim a run for this delivery, or return False if it was already claimed or finished.
|
|
603
|
+
|
|
604
|
+
Pub/Sub push delivers at least once, so a run can arrive again after a deploy or a lost ack.
|
|
605
|
+
A create-only put (GCS ``ifGenerationMatch=0``) lets exactly one delivery win without
|
|
606
|
+
needing delete permission on the results bucket. A claim is never released, so a delivery
|
|
607
|
+
whose attempt died is not retried; the server's run timeout covers that case.
|
|
608
|
+
"""
|
|
609
|
+
store = obstore.store.from_url(results_url)
|
|
610
|
+
try:
|
|
611
|
+
await store.head_async("result.json")
|
|
612
|
+
except FileNotFoundError:
|
|
613
|
+
pass
|
|
614
|
+
else:
|
|
615
|
+
return False # finished before claims existed, or the claim's attempt wrote it
|
|
616
|
+
try:
|
|
617
|
+
await store.put_async(RUN_LOCK_FILENAME, b"{}", mode="create")
|
|
618
|
+
except AlreadyExistsError:
|
|
619
|
+
return False
|
|
620
|
+
return True
|
|
621
|
+
|
|
622
|
+
|
|
593
623
|
async def upload_error_to_gcs(error_details: dict[str, Any], results_url: str) -> None:
|
|
594
624
|
"""Upload error details to Google Cloud Storage.
|
|
595
625
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|