labtasker-client 2.3.0__tar.gz → 2.4.1__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.
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/PKG-INFO +4 -2
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/pyproject.toml +5 -2
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/__init__.py +3 -1
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/cli.py +24 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/client.py +18 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/execution.py +40 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/models.py +35 -3
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/worker.py +45 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/.gitignore +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/LICENSE +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/__main__.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/api.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/binding.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/command_template.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/command_worker.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/config.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/errors.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/journal.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/local.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/observations.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/paths.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/py.typed +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/tee.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/types.py +0 -0
- {labtasker_client-2.3.0 → labtasker_client-2.4.1}/src/labtasker/validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labtasker-client
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: A small task queue for parallel model inference and evaluation
|
|
5
5
|
Project-URL: Homepage, https://github.com/luocfprime/labtasker
|
|
6
6
|
Project-URL: Repository, https://github.com/luocfprime/labtasker.git
|
|
@@ -8,10 +8,12 @@ Author-email: lcf <luocfprime@gmail.com>
|
|
|
8
8
|
License-Expression: Apache-2.0
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
|
-
Requires-Dist: httpx
|
|
11
|
+
Requires-Dist: httpx<1,>=0.25.2
|
|
12
12
|
Requires-Dist: packaging>=21.3
|
|
13
13
|
Requires-Dist: pydantic<3,>=2.12; python_version >= '3.14'
|
|
14
14
|
Requires-Dist: pydantic<3,>=2.5.3; python_version < '3.13'
|
|
15
15
|
Requires-Dist: pydantic<3,>=2.9; python_version >= '3.13' and python_version < '3.14'
|
|
16
16
|
Requires-Dist: tomli>=2.0.1; python_version < '3.11'
|
|
17
17
|
Requires-Dist: typer<1,>=0.19
|
|
18
|
+
Provides-Extra: socks
|
|
19
|
+
Requires-Dist: socksio==1.*; extra == 'socks'
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "labtasker-client"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.1"
|
|
8
8
|
description = "A small task queue for parallel model inference and evaluation"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
12
|
authors = [{ name = "lcf", email = "luocfprime@gmail.com" }]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"httpx
|
|
14
|
+
"httpx>=0.25.2,<1",
|
|
15
15
|
"packaging>=21.3",
|
|
16
16
|
"pydantic>=2.5.3,<3; python_version < '3.13'",
|
|
17
17
|
"pydantic>=2.9,<3; python_version >= '3.13' and python_version < '3.14'",
|
|
@@ -20,6 +20,9 @@ dependencies = [
|
|
|
20
20
|
"typer>=0.19,<1",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
socks = ["socksio==1.*"]
|
|
25
|
+
|
|
23
26
|
[project.urls]
|
|
24
27
|
Homepage = "https://github.com/luocfprime/labtasker"
|
|
25
28
|
Repository = "https://github.com/luocfprime/labtasker.git"
|
|
@@ -30,6 +30,7 @@ from labtasker.errors import (
|
|
|
30
30
|
from labtasker.execution import (
|
|
31
31
|
cancellation_requested,
|
|
32
32
|
finish,
|
|
33
|
+
report_progress,
|
|
33
34
|
set_force_stop_timeout,
|
|
34
35
|
task_info,
|
|
35
36
|
)
|
|
@@ -48,7 +49,7 @@ from labtasker.models import (
|
|
|
48
49
|
from labtasker.types import JSONValue, TaskOrderField, TaskStatus, TaskUpdate
|
|
49
50
|
from labtasker.worker import loop
|
|
50
51
|
|
|
51
|
-
__version__ = "2.
|
|
52
|
+
__version__ = "2.4.1"
|
|
52
53
|
|
|
53
54
|
__all__ = [
|
|
54
55
|
"APIError",
|
|
@@ -87,6 +88,7 @@ __all__ = [
|
|
|
87
88
|
"list_tasks",
|
|
88
89
|
"list_workers",
|
|
89
90
|
"loop",
|
|
91
|
+
"report_progress",
|
|
90
92
|
"requeue_task",
|
|
91
93
|
"set_force_stop_timeout",
|
|
92
94
|
"submit_task",
|
|
@@ -15,6 +15,7 @@ from labtasker.command_template import TemplateSyntaxError
|
|
|
15
15
|
from labtasker.command_worker import run_command_worker
|
|
16
16
|
from labtasker.config import resolve_config
|
|
17
17
|
from labtasker.errors import LabtaskerError
|
|
18
|
+
from labtasker.execution import report_progress as report_current_progress
|
|
18
19
|
from labtasker.types import TaskOrderField, TaskStatus, TaskUpdate
|
|
19
20
|
from labtasker.validation import RequestValidationError, validate_grouping, validate_json_object
|
|
20
21
|
|
|
@@ -169,6 +170,29 @@ def worker_loop(
|
|
|
169
170
|
raise typer.Exit(1) from error
|
|
170
171
|
|
|
171
172
|
|
|
173
|
+
@app.command("progress")
|
|
174
|
+
def progress_report(
|
|
175
|
+
data: Annotated[
|
|
176
|
+
str,
|
|
177
|
+
typer.Option(help="Latest progress as one strict JSON object."),
|
|
178
|
+
],
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Replace the current Task run's progress snapshot.
|
|
181
|
+
|
|
182
|
+
This command is available inside a command launched by ``labtasker loop``.
|
|
183
|
+
It prints whether the best-effort report was accepted; transport failures
|
|
184
|
+
and confirmed revocation return ``reported: false`` without failing the
|
|
185
|
+
command workload.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
progress = _json_object(data, option="--data")
|
|
189
|
+
reported = _invoke(lambda: report_current_progress(progress))
|
|
190
|
+
except RuntimeError as error:
|
|
191
|
+
typer.echo(str(error), err=True)
|
|
192
|
+
raise typer.Exit(1) from error
|
|
193
|
+
_write_json({"reported": reported})
|
|
194
|
+
|
|
195
|
+
|
|
172
196
|
@task_app.command("submit")
|
|
173
197
|
def task_submit(
|
|
174
198
|
args: Annotated[
|
|
@@ -589,6 +589,24 @@ class Client:
|
|
|
589
589
|
parser=lambda response: _parse_none(response, {204}),
|
|
590
590
|
)
|
|
591
591
|
|
|
592
|
+
def _report_progress(
|
|
593
|
+
self,
|
|
594
|
+
*,
|
|
595
|
+
task_id: str,
|
|
596
|
+
run_id: str,
|
|
597
|
+
progress: dict[str, JSONValue],
|
|
598
|
+
queue: str | None = None,
|
|
599
|
+
) -> None:
|
|
600
|
+
normalized = validate_json_object(progress, field="progress")
|
|
601
|
+
self._run_action(
|
|
602
|
+
"progress",
|
|
603
|
+
task_id=task_id,
|
|
604
|
+
run_id=run_id,
|
|
605
|
+
queue=queue,
|
|
606
|
+
body={"progress": normalized},
|
|
607
|
+
parser=lambda response: _parse_none(response, {204}),
|
|
608
|
+
)
|
|
609
|
+
|
|
592
610
|
def _fail(
|
|
593
611
|
self,
|
|
594
612
|
*,
|
|
@@ -17,6 +17,7 @@ from labtasker.types import JSONValue
|
|
|
17
17
|
from labtasker.validation import RequestValidationError, validate_json_object
|
|
18
18
|
|
|
19
19
|
CompletionReporter = Callable[[dict[str, JSONValue]], bool]
|
|
20
|
+
ProgressReporter = Callable[[dict[str, JSONValue]], bool]
|
|
20
21
|
ContextKind = Literal["python", "command"]
|
|
21
22
|
|
|
22
23
|
|
|
@@ -128,12 +129,14 @@ class ExecutionContext:
|
|
|
128
129
|
journal: LocalRunJournal,
|
|
129
130
|
reporter: CompletionReporter,
|
|
130
131
|
control: RunControl | None,
|
|
132
|
+
progress_reporter: ProgressReporter | None = None,
|
|
131
133
|
) -> None:
|
|
132
134
|
self.info = info
|
|
133
135
|
self.kind = kind
|
|
134
136
|
self.journal = journal
|
|
135
137
|
self.reporter = reporter
|
|
136
138
|
self.control = control
|
|
139
|
+
self.progress_reporter = progress_reporter
|
|
137
140
|
self._lock = threading.Lock()
|
|
138
141
|
self._finish_started = False
|
|
139
142
|
self._finished = False
|
|
@@ -161,6 +164,17 @@ class ExecutionContext:
|
|
|
161
164
|
self.control.complete()
|
|
162
165
|
_best_effort_journal(self.journal.acknowledged)
|
|
163
166
|
|
|
167
|
+
def report_progress(self, progress: dict[str, JSONValue]) -> bool:
|
|
168
|
+
with self._lock:
|
|
169
|
+
if self._finished:
|
|
170
|
+
raise RuntimeError("The current run has already completed.")
|
|
171
|
+
if self.control is not None and not self.control.active:
|
|
172
|
+
return False
|
|
173
|
+
reporter = self.progress_reporter
|
|
174
|
+
if reporter is None:
|
|
175
|
+
raise RuntimeError("Progress reporting is unavailable for this execution.")
|
|
176
|
+
return reporter(progress)
|
|
177
|
+
|
|
164
178
|
|
|
165
179
|
_CONTEXT_LOCK = threading.RLock()
|
|
166
180
|
_ACTIVE_CONTEXT: ExecutionContext | None = None
|
|
@@ -212,6 +226,20 @@ def finish(
|
|
|
212
226
|
context.finish(normalized)
|
|
213
227
|
|
|
214
228
|
|
|
229
|
+
def report_progress(
|
|
230
|
+
progress: dict[str, JSONValue],
|
|
231
|
+
*,
|
|
232
|
+
skip_if_no_labtasker: bool = False,
|
|
233
|
+
) -> bool:
|
|
234
|
+
context = _get_context()
|
|
235
|
+
if context is None:
|
|
236
|
+
if skip_if_no_labtasker:
|
|
237
|
+
return False
|
|
238
|
+
raise RuntimeError("No active Labtasker Task execution is available.")
|
|
239
|
+
normalized = validate_json_object(progress, field="progress")
|
|
240
|
+
return context.report_progress(normalized)
|
|
241
|
+
|
|
242
|
+
|
|
215
243
|
def cancellation_requested() -> bool:
|
|
216
244
|
context = _require_python_context()
|
|
217
245
|
if context.finished or context.control is None:
|
|
@@ -340,12 +368,24 @@ def _load_environment_context() -> ExecutionContext | None:
|
|
|
340
368
|
result=result,
|
|
341
369
|
)
|
|
342
370
|
|
|
371
|
+
def progress_reporter(progress: dict[str, JSONValue]) -> bool:
|
|
372
|
+
from labtasker.worker import report_progress_once
|
|
373
|
+
|
|
374
|
+
return report_progress_once(
|
|
375
|
+
client,
|
|
376
|
+
queue=values["queue"] or "",
|
|
377
|
+
task_id=values["task_id"] or "",
|
|
378
|
+
run_id=run_id,
|
|
379
|
+
progress=progress,
|
|
380
|
+
)
|
|
381
|
+
|
|
343
382
|
_ENV_CONTEXT = ExecutionContext(
|
|
344
383
|
info=info,
|
|
345
384
|
kind="command",
|
|
346
385
|
journal=journal,
|
|
347
386
|
reporter=report,
|
|
348
387
|
control=None,
|
|
388
|
+
progress_reporter=progress_reporter,
|
|
349
389
|
)
|
|
350
390
|
return _ENV_CONTEXT
|
|
351
391
|
|
|
@@ -68,6 +68,9 @@ class Task(ResponseModel):
|
|
|
68
68
|
max_attempts: int
|
|
69
69
|
routes: list[str]
|
|
70
70
|
result: dict[str, JSONValue]
|
|
71
|
+
progress: dict[str, JSONValue] | None = None
|
|
72
|
+
progress_updated_at: datetime | None = None
|
|
73
|
+
progress_attempt: int | None = None
|
|
71
74
|
last_error: LastError | None
|
|
72
75
|
last_route: str | None
|
|
73
76
|
created_at: datetime
|
|
@@ -90,9 +93,15 @@ class Task(ResponseModel):
|
|
|
90
93
|
def validate_name(cls, value: str | None) -> str | None:
|
|
91
94
|
return validate_task_name(value)
|
|
92
95
|
|
|
93
|
-
@field_validator("args", "metadata", "result")
|
|
96
|
+
@field_validator("args", "metadata", "result", "progress")
|
|
94
97
|
@classmethod
|
|
95
|
-
def validate_objects(
|
|
98
|
+
def validate_objects(
|
|
99
|
+
cls,
|
|
100
|
+
value: dict[str, JSONValue] | None,
|
|
101
|
+
info: object,
|
|
102
|
+
) -> dict[str, JSONValue] | None:
|
|
103
|
+
if value is None:
|
|
104
|
+
return None
|
|
96
105
|
return validate_json_object(value, field=getattr(info, "field_name", "task"))
|
|
97
106
|
|
|
98
107
|
@field_validator("priority", "attempt")
|
|
@@ -100,6 +109,23 @@ class Task(ResponseModel):
|
|
|
100
109
|
def validate_numbers(cls, value: int, info: object) -> int:
|
|
101
110
|
return validate_int64(value, field=getattr(info, "field_name", "task"))
|
|
102
111
|
|
|
112
|
+
@field_validator("progress_attempt")
|
|
113
|
+
@classmethod
|
|
114
|
+
def validate_progress_attempt(cls, value: int | None) -> int | None:
|
|
115
|
+
if value is None:
|
|
116
|
+
return None
|
|
117
|
+
value = validate_int64(value, field="progress_attempt")
|
|
118
|
+
if value < 0:
|
|
119
|
+
raise ValueError("progress_attempt must be non-negative")
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
@model_validator(mode="after")
|
|
123
|
+
def validate_progress_fields(self) -> Task:
|
|
124
|
+
fields = (self.progress, self.progress_updated_at, self.progress_attempt)
|
|
125
|
+
if any(value is None for value in fields) and any(value is not None for value in fields):
|
|
126
|
+
raise ValueError("progress fields must be all null or all present")
|
|
127
|
+
return self
|
|
128
|
+
|
|
103
129
|
@field_validator("max_attempts")
|
|
104
130
|
@classmethod
|
|
105
131
|
def validate_max_attempts(cls, value: int) -> int:
|
|
@@ -118,7 +144,13 @@ class Task(ResponseModel):
|
|
|
118
144
|
def validate_last_route(cls, value: str | None) -> str | None:
|
|
119
145
|
return None if value is None else validate_identifier(value, field="last_route")
|
|
120
146
|
|
|
121
|
-
@field_validator(
|
|
147
|
+
@field_validator(
|
|
148
|
+
"created_at",
|
|
149
|
+
"updated_at",
|
|
150
|
+
"started_at",
|
|
151
|
+
"finished_at",
|
|
152
|
+
"progress_updated_at",
|
|
153
|
+
)
|
|
122
154
|
@classmethod
|
|
123
155
|
def validate_times(cls, value: datetime | None) -> datetime | None:
|
|
124
156
|
return None if value is None else _utc_datetime(value)
|
|
@@ -19,6 +19,7 @@ from labtasker.errors import (
|
|
|
19
19
|
APIError,
|
|
20
20
|
ConfigError,
|
|
21
21
|
FatalWorkerError,
|
|
22
|
+
LabtaskerError,
|
|
22
23
|
TransientError,
|
|
23
24
|
TransportError,
|
|
24
25
|
)
|
|
@@ -260,6 +261,16 @@ def _run_python_claim(
|
|
|
260
261
|
control.revoke("stale_run")
|
|
261
262
|
return accepted
|
|
262
263
|
|
|
264
|
+
def progress_reporter(progress: dict[str, JSONValue]) -> bool:
|
|
265
|
+
return report_progress_once(
|
|
266
|
+
client,
|
|
267
|
+
queue=queue,
|
|
268
|
+
task_id=claim.task.id,
|
|
269
|
+
run_id=claim.run_id,
|
|
270
|
+
progress=progress,
|
|
271
|
+
control=control,
|
|
272
|
+
)
|
|
273
|
+
|
|
263
274
|
info = TaskInfo(
|
|
264
275
|
**claim.task.model_dump(),
|
|
265
276
|
run_id=claim.run_id,
|
|
@@ -271,6 +282,7 @@ def _run_python_claim(
|
|
|
271
282
|
journal=journal,
|
|
272
283
|
reporter=report_complete,
|
|
273
284
|
control=control,
|
|
285
|
+
progress_reporter=progress_reporter,
|
|
274
286
|
)
|
|
275
287
|
heartbeat = Heartbeat(
|
|
276
288
|
client,
|
|
@@ -358,6 +370,39 @@ def report_complete_until_resolved(
|
|
|
358
370
|
)
|
|
359
371
|
|
|
360
372
|
|
|
373
|
+
def report_progress_once(
|
|
374
|
+
client: Client,
|
|
375
|
+
*,
|
|
376
|
+
queue: str,
|
|
377
|
+
task_id: str,
|
|
378
|
+
run_id: str,
|
|
379
|
+
progress: dict[str, JSONValue],
|
|
380
|
+
control: RunControl | None = None,
|
|
381
|
+
) -> bool:
|
|
382
|
+
try:
|
|
383
|
+
client._report_progress(
|
|
384
|
+
task_id=task_id,
|
|
385
|
+
run_id=run_id,
|
|
386
|
+
progress=progress,
|
|
387
|
+
queue=queue,
|
|
388
|
+
)
|
|
389
|
+
except APIError as error:
|
|
390
|
+
if error.code == "run_finalized" and error.details.get("action") == "complete":
|
|
391
|
+
if control is not None:
|
|
392
|
+
control.complete()
|
|
393
|
+
elif error.code in {"run_finalized", "stale_run"} and control is not None:
|
|
394
|
+
control.revoke(str(error.details.get("action", error.code)))
|
|
395
|
+
logger.warning("Progress report rejected; continuing Task: %s", error.message)
|
|
396
|
+
return False
|
|
397
|
+
except TransportError as error:
|
|
398
|
+
logger.warning("Progress report transport error; continuing Task: %s", error.message)
|
|
399
|
+
return False
|
|
400
|
+
except LabtaskerError as error:
|
|
401
|
+
logger.warning("Progress report failed; continuing Task: %s", error.message)
|
|
402
|
+
return False
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
|
|
361
406
|
def _report_complete(
|
|
362
407
|
client: Client,
|
|
363
408
|
journal: LocalRunJournal,
|
|
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
|