taskferry-cloudrun 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.
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/CHANGELOG.md +10 -0
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/PKG-INFO +1 -1
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/pyproject.toml +1 -1
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/src/taskferry_cloudrun/backend.py +108 -12
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/.gitignore +0 -0
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/LICENSE +0 -0
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/README.md +0 -0
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/src/taskferry_cloudrun/__init__.py +0 -0
- {taskferry_cloudrun-0.3.0 → taskferry_cloudrun-0.4.0}/src/taskferry_cloudrun/py.typed +0 -0
|
@@ -4,6 +4,16 @@ All notable changes to `taskferry-cloudrun` are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
5
5
|
this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] — 2026-09-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Native async** submit, state and cancel via the run_v2 async clients
|
|
12
|
+
(`JobsAsyncClient`, `ExecutionsAsyncClient`). `asubmit`/`aget`/`acancel` use
|
|
13
|
+
them when available, skipping the worker thread, and fall back to the thread
|
|
14
|
+
path when the SDK is absent. No new dependency — the async clients ship with
|
|
15
|
+
`google-cloud-run`.
|
|
16
|
+
|
|
7
17
|
## [0.3.0] — 2026-09-19
|
|
8
18
|
|
|
9
19
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskferry-cloudrun
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Google Cloud Run Jobs backend for Taskferry — serverless batch workloads.
|
|
5
5
|
Project-URL: Homepage, https://github.com/xiidigital/taskferry
|
|
6
6
|
Project-URL: Documentation, https://taskferry.dev
|
|
@@ -140,6 +140,8 @@ class CloudRunJobBackend(BaseBackend):
|
|
|
140
140
|
location: str | None = None,
|
|
141
141
|
jobs_client: Any = None,
|
|
142
142
|
executions_client: Any = None,
|
|
143
|
+
jobs_async_client: Any = None,
|
|
144
|
+
executions_async_client: Any = None,
|
|
143
145
|
logging_client: Any = None,
|
|
144
146
|
name: str = "cloudrun",
|
|
145
147
|
tracked_ids: int = 10_000,
|
|
@@ -153,6 +155,8 @@ class CloudRunJobBackend(BaseBackend):
|
|
|
153
155
|
self._location = location
|
|
154
156
|
self._jobs_client = jobs_client
|
|
155
157
|
self._executions_client = executions_client
|
|
158
|
+
self._jobs_async_client = jobs_async_client
|
|
159
|
+
self._executions_async_client = executions_async_client
|
|
156
160
|
self._logging_client = logging_client
|
|
157
161
|
self._name = name
|
|
158
162
|
# Cloud Run names executions itself, so remember which of its names goes
|
|
@@ -195,6 +199,18 @@ class CloudRunJobBackend(BaseBackend):
|
|
|
195
199
|
self._logging_client = _logging_v2().Client(project=self._project)
|
|
196
200
|
return self._logging_client
|
|
197
201
|
|
|
202
|
+
def _async_jobs(self) -> Any:
|
|
203
|
+
"""The async Jobs client: injected, lazily built, or ``None`` if the SDK
|
|
204
|
+
is absent (the caller then falls back to the thread path)."""
|
|
205
|
+
if self._jobs_async_client is None and _has_run_v2():
|
|
206
|
+
self._jobs_async_client = _run_v2().JobsAsyncClient()
|
|
207
|
+
return self._jobs_async_client
|
|
208
|
+
|
|
209
|
+
def _async_executions(self) -> Any:
|
|
210
|
+
if self._executions_async_client is None and _has_run_v2():
|
|
211
|
+
self._executions_async_client = _run_v2().ExecutionsAsyncClient()
|
|
212
|
+
return self._executions_async_client
|
|
213
|
+
|
|
198
214
|
# -- resource names ----------------------------------------------------------- #
|
|
199
215
|
def job_resource(self, spec: JobSpec) -> str:
|
|
200
216
|
"""Fully-qualified Cloud Run Job resource this spec targets."""
|
|
@@ -204,25 +220,28 @@ class CloudRunJobBackend(BaseBackend):
|
|
|
204
220
|
return f"projects/{self._project}/locations/{self._location}/jobs/{spec.job}"
|
|
205
221
|
|
|
206
222
|
# -- submission ---------------------------------------------------------------- #
|
|
207
|
-
def
|
|
208
|
-
assert isinstance(spec, JobSpec)
|
|
223
|
+
def _submit_request(self, spec: JobSpec) -> dict[str, Any]:
|
|
209
224
|
request: dict[str, Any] = {"name": self.job_resource(spec)}
|
|
210
225
|
overrides = self._overrides(spec)
|
|
211
226
|
if overrides:
|
|
212
227
|
request["overrides"] = overrides
|
|
228
|
+
return request
|
|
213
229
|
|
|
230
|
+
def _submit(self, spec: ExecutionSpec) -> Execution:
|
|
231
|
+
assert isinstance(spec, JobSpec)
|
|
214
232
|
try:
|
|
215
|
-
operation = self._jobs().run_job(request=
|
|
233
|
+
operation = self._jobs().run_job(request=self._submit_request(spec))
|
|
216
234
|
except Exception as exc:
|
|
217
235
|
raise SubmissionError(
|
|
218
236
|
f"Cloud Run could not start job {spec.job!r} in {self._location}: {exc}",
|
|
219
237
|
backend=self._name,
|
|
220
238
|
) from exc
|
|
239
|
+
return self._execution_from_operation(spec, operation)
|
|
221
240
|
|
|
241
|
+
def _execution_from_operation(self, spec: JobSpec, operation: Any) -> Execution:
|
|
222
242
|
external_id = _execution_name(operation)
|
|
223
243
|
execution_id = new_execution_id(ExecutionKind.JOB)
|
|
224
244
|
self._ids.remember(str(execution_id), external_id)
|
|
225
|
-
|
|
226
245
|
return Execution(
|
|
227
246
|
id=execution_id,
|
|
228
247
|
kind=ExecutionKind.JOB,
|
|
@@ -264,7 +283,7 @@ class CloudRunJobBackend(BaseBackend):
|
|
|
264
283
|
return overrides
|
|
265
284
|
|
|
266
285
|
# -- observation ------------------------------------------------------------------ #
|
|
267
|
-
def
|
|
286
|
+
def _resolve_external_id(self, execution_id: ExecutionId) -> str:
|
|
268
287
|
external_id = self._ids.resolve(str(execution_id))
|
|
269
288
|
if external_id is None:
|
|
270
289
|
raise ExecutionNotFound(
|
|
@@ -273,17 +292,28 @@ class CloudRunJobBackend(BaseBackend):
|
|
|
273
292
|
"from another process",
|
|
274
293
|
backend=self._name,
|
|
275
294
|
)
|
|
295
|
+
return external_id
|
|
296
|
+
|
|
297
|
+
def _get(self, execution_id: ExecutionId) -> Execution:
|
|
298
|
+
external_id = self._resolve_external_id(execution_id)
|
|
276
299
|
try:
|
|
277
300
|
remote = self._executions().get_execution(name=external_id)
|
|
278
301
|
except Exception as exc:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
f"Cloud Run has no execution {external_id!r}", backend=self._name
|
|
282
|
-
) from exc
|
|
283
|
-
raise BackendError(
|
|
284
|
-
f"Cloud Run could not read execution {external_id!r}: {exc}", backend=self._name
|
|
285
|
-
) from exc
|
|
302
|
+
raise self._read_error(external_id, exc) from exc
|
|
303
|
+
return self._execution_from_remote(execution_id, external_id, remote)
|
|
286
304
|
|
|
305
|
+
def _read_error(self, external_id: str, exc: Exception) -> Exception:
|
|
306
|
+
if _is_not_found(exc):
|
|
307
|
+
return ExecutionNotFound(
|
|
308
|
+
f"Cloud Run has no execution {external_id!r}", backend=self._name
|
|
309
|
+
)
|
|
310
|
+
return BackendError(
|
|
311
|
+
f"Cloud Run could not read execution {external_id!r}: {exc}", backend=self._name
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _execution_from_remote(
|
|
315
|
+
self, execution_id: ExecutionId, external_id: str, remote: Any
|
|
316
|
+
) -> Execution:
|
|
287
317
|
state, error = map_execution_state(remote)
|
|
288
318
|
return Execution(
|
|
289
319
|
id=execution_id,
|
|
@@ -320,6 +350,60 @@ class CloudRunJobBackend(BaseBackend):
|
|
|
320
350
|
) from exc
|
|
321
351
|
return self._get(execution_id)
|
|
322
352
|
|
|
353
|
+
# -- native async (run_v2 async clients) -------------------------------------- #
|
|
354
|
+
async def asubmit(self, spec: ExecutionSpec) -> Execution:
|
|
355
|
+
"""Native async submit via ``JobsAsyncClient``, else the thread path."""
|
|
356
|
+
client = self._async_jobs()
|
|
357
|
+
if client is None:
|
|
358
|
+
return await super().asubmit(spec)
|
|
359
|
+
assert isinstance(spec, JobSpec)
|
|
360
|
+
self.validate(spec)
|
|
361
|
+
self.hooks.before_submit(spec, self.name)
|
|
362
|
+
try:
|
|
363
|
+
operation = await client.run_job(request=self._submit_request(spec))
|
|
364
|
+
execution = self._execution_from_operation(spec, operation)
|
|
365
|
+
except Exception as exc:
|
|
366
|
+
error = SubmissionError(
|
|
367
|
+
f"Cloud Run could not start job {spec.job!r} in {self._location}: {exc}",
|
|
368
|
+
backend=self._name,
|
|
369
|
+
)
|
|
370
|
+
self.hooks.on_submit_error(spec, self.name, error)
|
|
371
|
+
raise error from exc
|
|
372
|
+
self.hooks.after_submit(spec, execution)
|
|
373
|
+
return execution
|
|
374
|
+
|
|
375
|
+
async def aget(self, execution_id: ExecutionId | str) -> Execution:
|
|
376
|
+
"""Native async state read via ``ExecutionsAsyncClient``."""
|
|
377
|
+
client = self._async_executions()
|
|
378
|
+
if client is None:
|
|
379
|
+
return await super().aget(execution_id)
|
|
380
|
+
self.capabilities.require(Capability.STATE)
|
|
381
|
+
eid = ExecutionId(str(execution_id))
|
|
382
|
+
external_id = self._resolve_external_id(eid)
|
|
383
|
+
try:
|
|
384
|
+
remote = await client.get_execution(name=external_id)
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
raise self._read_error(external_id, exc) from exc
|
|
387
|
+
return self._execution_from_remote(eid, external_id, remote)
|
|
388
|
+
|
|
389
|
+
async def acancel(self, execution_id: ExecutionId | str) -> Execution:
|
|
390
|
+
"""Native async cancel via ``ExecutionsAsyncClient``."""
|
|
391
|
+
client = self._async_executions()
|
|
392
|
+
if client is None:
|
|
393
|
+
return await super().acancel(execution_id)
|
|
394
|
+
self.capabilities.require(Capability.CANCEL)
|
|
395
|
+
eid = ExecutionId(str(execution_id))
|
|
396
|
+
external_id = self._resolve_external_id(eid)
|
|
397
|
+
try:
|
|
398
|
+
await client.cancel_execution(name=external_id)
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
raise BackendError(
|
|
401
|
+
f"Cloud Run could not cancel execution {external_id!r}: {exc}", backend=self._name
|
|
402
|
+
) from exc
|
|
403
|
+
execution = await self.aget(eid)
|
|
404
|
+
self.hooks.on_cancel(execution)
|
|
405
|
+
return execution
|
|
406
|
+
|
|
323
407
|
def logs_uri(self, external_id: str) -> str:
|
|
324
408
|
"""A Cloud Logging console link for an execution. No request is made."""
|
|
325
409
|
return (
|
|
@@ -424,6 +508,16 @@ def _run_v2() -> Any:
|
|
|
424
508
|
return run_v2
|
|
425
509
|
|
|
426
510
|
|
|
511
|
+
def _has_run_v2() -> bool:
|
|
512
|
+
"""Whether the Cloud Run SDK is importable, without importing it."""
|
|
513
|
+
from importlib.util import find_spec
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
return find_spec("google.cloud.run_v2") is not None
|
|
517
|
+
except ModuleNotFoundError:
|
|
518
|
+
return False
|
|
519
|
+
|
|
520
|
+
|
|
427
521
|
def _logging_v2() -> Any:
|
|
428
522
|
"""Import ``google.cloud.logging_v2`` lazily, with an actionable error."""
|
|
429
523
|
try:
|
|
@@ -500,6 +594,8 @@ def make_backend(**options: Any) -> CloudRunJobBackend:
|
|
|
500
594
|
location=options.get("location"),
|
|
501
595
|
jobs_client=options.get("jobs_client"),
|
|
502
596
|
executions_client=options.get("executions_client"),
|
|
597
|
+
jobs_async_client=options.get("jobs_async_client"),
|
|
598
|
+
executions_async_client=options.get("executions_async_client"),
|
|
503
599
|
logging_client=options.get("logging_client"),
|
|
504
600
|
name=str(options.get("name", "cloudrun")),
|
|
505
601
|
tracked_ids=int(options.get("tracked_ids", 10_000)),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|