labtasker-client 2.0.0__py3-none-any.whl
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/__init__.py +77 -0
- labtasker/__main__.py +3 -0
- labtasker/api.py +122 -0
- labtasker/binding.py +184 -0
- labtasker/cli.py +506 -0
- labtasker/client.py +725 -0
- labtasker/command_template.py +173 -0
- labtasker/command_worker.py +492 -0
- labtasker/config.py +193 -0
- labtasker/errors.py +56 -0
- labtasker/execution.py +380 -0
- labtasker/journal.py +314 -0
- labtasker/local.py +190 -0
- labtasker/models.py +218 -0
- labtasker/paths.py +34 -0
- labtasker/py.typed +1 -0
- labtasker/tee.py +128 -0
- labtasker/types.py +31 -0
- labtasker/validation.py +207 -0
- labtasker/worker.py +473 -0
- labtasker_client-2.0.0.dist-info/METADATA +13 -0
- labtasker_client-2.0.0.dist-info/RECORD +25 -0
- labtasker_client-2.0.0.dist-info/WHEEL +4 -0
- labtasker_client-2.0.0.dist-info/entry_points.txt +2 -0
- labtasker_client-2.0.0.dist-info/licenses/LICENSE +201 -0
labtasker/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Public package for the Labtasker v2 client and Worker runtime."""
|
|
2
|
+
|
|
3
|
+
from labtasker.api import (
|
|
4
|
+
cancel_task,
|
|
5
|
+
count_tasks,
|
|
6
|
+
create_queue,
|
|
7
|
+
delete_queue,
|
|
8
|
+
delete_task,
|
|
9
|
+
get_task,
|
|
10
|
+
list_queues,
|
|
11
|
+
list_tasks,
|
|
12
|
+
requeue_task,
|
|
13
|
+
submit_task,
|
|
14
|
+
update_task,
|
|
15
|
+
update_tasks,
|
|
16
|
+
)
|
|
17
|
+
from labtasker.binding import TaskArg
|
|
18
|
+
from labtasker.client import Client
|
|
19
|
+
from labtasker.errors import (
|
|
20
|
+
APIError,
|
|
21
|
+
ConfigError,
|
|
22
|
+
FatalWorkerError,
|
|
23
|
+
LabtaskerError,
|
|
24
|
+
TaskError,
|
|
25
|
+
TransientError,
|
|
26
|
+
TransportError,
|
|
27
|
+
)
|
|
28
|
+
from labtasker.execution import (
|
|
29
|
+
cancellation_requested,
|
|
30
|
+
finish,
|
|
31
|
+
set_force_stop_timeout,
|
|
32
|
+
task_info,
|
|
33
|
+
)
|
|
34
|
+
from labtasker.models import BulkUpdateResult, LastError, Queue, Task, TaskInfo, TaskPage
|
|
35
|
+
from labtasker.types import JSONValue, TaskOrderField, TaskStatus, TaskUpdate
|
|
36
|
+
from labtasker.worker import loop
|
|
37
|
+
|
|
38
|
+
__version__ = "2.0.0"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"APIError",
|
|
42
|
+
"BulkUpdateResult",
|
|
43
|
+
"Client",
|
|
44
|
+
"ConfigError",
|
|
45
|
+
"FatalWorkerError",
|
|
46
|
+
"JSONValue",
|
|
47
|
+
"LabtaskerError",
|
|
48
|
+
"LastError",
|
|
49
|
+
"Queue",
|
|
50
|
+
"Task",
|
|
51
|
+
"TaskArg",
|
|
52
|
+
"TaskError",
|
|
53
|
+
"TaskInfo",
|
|
54
|
+
"TaskOrderField",
|
|
55
|
+
"TaskPage",
|
|
56
|
+
"TaskStatus",
|
|
57
|
+
"TaskUpdate",
|
|
58
|
+
"TransientError",
|
|
59
|
+
"TransportError",
|
|
60
|
+
"cancel_task",
|
|
61
|
+
"cancellation_requested",
|
|
62
|
+
"count_tasks",
|
|
63
|
+
"create_queue",
|
|
64
|
+
"delete_queue",
|
|
65
|
+
"delete_task",
|
|
66
|
+
"finish",
|
|
67
|
+
"get_task",
|
|
68
|
+
"list_queues",
|
|
69
|
+
"list_tasks",
|
|
70
|
+
"loop",
|
|
71
|
+
"requeue_task",
|
|
72
|
+
"set_force_stop_timeout",
|
|
73
|
+
"submit_task",
|
|
74
|
+
"task_info",
|
|
75
|
+
"update_task",
|
|
76
|
+
"update_tasks",
|
|
77
|
+
]
|
labtasker/__main__.py
ADDED
labtasker/api.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from threading import Lock
|
|
4
|
+
|
|
5
|
+
from labtasker.client import Client
|
|
6
|
+
from labtasker.models import BulkUpdateResult, Queue, Task, TaskPage
|
|
7
|
+
from labtasker.types import JSONValue, TaskOrderField, TaskStatus, TaskUpdate
|
|
8
|
+
|
|
9
|
+
_default_client: Client | None = None
|
|
10
|
+
_default_client_lock = Lock()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def submit_task(
|
|
14
|
+
args: dict[str, JSONValue] | None = None,
|
|
15
|
+
*,
|
|
16
|
+
name: str | None = None,
|
|
17
|
+
metadata: dict[str, JSONValue] | None = None,
|
|
18
|
+
priority: int = 0,
|
|
19
|
+
max_attempts: int = 3,
|
|
20
|
+
routes: list[str] | None = None,
|
|
21
|
+
task_id: str | None = None,
|
|
22
|
+
queue: str | None = None,
|
|
23
|
+
) -> Task:
|
|
24
|
+
return _client().submit_task(
|
|
25
|
+
args,
|
|
26
|
+
name=name,
|
|
27
|
+
metadata=metadata,
|
|
28
|
+
priority=priority,
|
|
29
|
+
max_attempts=max_attempts,
|
|
30
|
+
routes=routes,
|
|
31
|
+
task_id=task_id,
|
|
32
|
+
queue=queue,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_task(task_id: str, *, queue: str | None = None) -> Task:
|
|
37
|
+
return _client().get_task(task_id, queue=queue)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def list_tasks(
|
|
41
|
+
*,
|
|
42
|
+
status: TaskStatus | None = None,
|
|
43
|
+
name: str | None = None,
|
|
44
|
+
filter: str | None = None,
|
|
45
|
+
order_by: TaskOrderField = "created_at",
|
|
46
|
+
descending: bool = True,
|
|
47
|
+
limit: int = 100,
|
|
48
|
+
cursor: str | None = None,
|
|
49
|
+
queue: str | None = None,
|
|
50
|
+
) -> TaskPage:
|
|
51
|
+
return _client().list_tasks(
|
|
52
|
+
status=status,
|
|
53
|
+
name=name,
|
|
54
|
+
filter=filter,
|
|
55
|
+
order_by=order_by,
|
|
56
|
+
descending=descending,
|
|
57
|
+
limit=limit,
|
|
58
|
+
cursor=cursor,
|
|
59
|
+
queue=queue,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def count_tasks(
|
|
64
|
+
*,
|
|
65
|
+
status: TaskStatus | None = None,
|
|
66
|
+
name: str | None = None,
|
|
67
|
+
filter: str | None = None,
|
|
68
|
+
queue: str | None = None,
|
|
69
|
+
) -> int:
|
|
70
|
+
return _client().count_tasks(status=status, name=name, filter=filter, queue=queue)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def update_task(
|
|
74
|
+
task_id: str,
|
|
75
|
+
changes: TaskUpdate,
|
|
76
|
+
*,
|
|
77
|
+
queue: str | None = None,
|
|
78
|
+
) -> Task:
|
|
79
|
+
return _client().update_task(task_id, changes, queue=queue)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def update_tasks(
|
|
83
|
+
*,
|
|
84
|
+
filter: str,
|
|
85
|
+
changes: TaskUpdate,
|
|
86
|
+
queue: str | None = None,
|
|
87
|
+
) -> BulkUpdateResult:
|
|
88
|
+
return _client().update_tasks(filter=filter, changes=changes, queue=queue)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cancel_task(task_id: str, *, queue: str | None = None) -> Task:
|
|
92
|
+
return _client().cancel_task(task_id, queue=queue)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def requeue_task(task_id: str, *, queue: str | None = None) -> Task:
|
|
96
|
+
return _client().requeue_task(task_id, queue=queue)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def delete_task(task_id: str, *, queue: str | None = None) -> None:
|
|
100
|
+
_client().delete_task(task_id, queue=queue)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def create_queue(name: str) -> Queue:
|
|
104
|
+
return _client().create_queue(name)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def list_queues() -> list[Queue]:
|
|
108
|
+
return _client().list_queues()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def delete_queue(name: str, *, cascade: bool = False) -> None:
|
|
112
|
+
_client().delete_queue(name, cascade=cascade)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _client() -> Client:
|
|
116
|
+
global _default_client
|
|
117
|
+
if _default_client is not None:
|
|
118
|
+
return _default_client
|
|
119
|
+
with _default_client_lock:
|
|
120
|
+
if _default_client is None:
|
|
121
|
+
_default_client = Client()
|
|
122
|
+
return _default_client
|
labtasker/binding.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, TypeVar, get_type_hints, overload
|
|
7
|
+
|
|
8
|
+
from pydantic import TypeAdapter, ValidationError
|
|
9
|
+
|
|
10
|
+
from labtasker.paths import PathError, parse_path, select_path
|
|
11
|
+
from labtasker.types import JSONValue
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
_MISSING = object()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class _TaskArgMarker:
|
|
19
|
+
default: object
|
|
20
|
+
path: str | None
|
|
21
|
+
resolver: Callable[[Any], Any] | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@overload
|
|
25
|
+
def TaskArg(
|
|
26
|
+
*,
|
|
27
|
+
default: T,
|
|
28
|
+
path: str | None = None,
|
|
29
|
+
resolver: None = None,
|
|
30
|
+
) -> T: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@overload
|
|
34
|
+
def TaskArg(
|
|
35
|
+
*,
|
|
36
|
+
default: object = _MISSING,
|
|
37
|
+
path: str | None = None,
|
|
38
|
+
resolver: Callable[[Any], T],
|
|
39
|
+
) -> T: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def TaskArg(
|
|
44
|
+
*,
|
|
45
|
+
default: object = _MISSING,
|
|
46
|
+
path: str | None = None,
|
|
47
|
+
resolver: None = None,
|
|
48
|
+
) -> Any: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def TaskArg(
|
|
52
|
+
*,
|
|
53
|
+
default: object = _MISSING,
|
|
54
|
+
path: str | None = None,
|
|
55
|
+
resolver: Callable[[Any], Any] | None = None,
|
|
56
|
+
) -> Any:
|
|
57
|
+
"""Mark one decorated Worker parameter as coming from Task args."""
|
|
58
|
+
return _TaskArgMarker(default=default, path=path, resolver=resolver)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BindingError(ValueError):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class _Injection:
|
|
67
|
+
name: str
|
|
68
|
+
path: tuple[str, ...]
|
|
69
|
+
default: object
|
|
70
|
+
resolver: Callable[[Any], Any] | None
|
|
71
|
+
adapter: TypeAdapter[Any] | None
|
|
72
|
+
|
|
73
|
+
def resolve(self, args: dict[str, JSONValue]) -> Any:
|
|
74
|
+
try:
|
|
75
|
+
value = select_path(args, self.path)
|
|
76
|
+
except PathError as error:
|
|
77
|
+
if self.default is _MISSING:
|
|
78
|
+
raise BindingError(f"TaskArg {self.name!r}: {error}") from error
|
|
79
|
+
value = self.default
|
|
80
|
+
if self.resolver is not None:
|
|
81
|
+
try:
|
|
82
|
+
value = self.resolver(value)
|
|
83
|
+
except Exception as error:
|
|
84
|
+
raise BindingError(f"TaskArg {self.name!r} resolver failed: {error}") from error
|
|
85
|
+
if self.adapter is not None:
|
|
86
|
+
try:
|
|
87
|
+
value = self.adapter.validate_python(value, strict=True)
|
|
88
|
+
except ValidationError as error:
|
|
89
|
+
message = f"TaskArg {self.name!r} failed strict validation: {error}"
|
|
90
|
+
raise BindingError(message) from error
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True, slots=True)
|
|
95
|
+
class CompiledBinding:
|
|
96
|
+
function: Callable[..., Any]
|
|
97
|
+
signature: inspect.Signature
|
|
98
|
+
injections: tuple[_Injection, ...]
|
|
99
|
+
|
|
100
|
+
def validate_invocation(self, args: tuple[object, ...], kwargs: dict[str, object]) -> None:
|
|
101
|
+
bound = self.signature.bind(*args, **kwargs)
|
|
102
|
+
supplied = {injection.name for injection in self.injections} & bound.arguments.keys()
|
|
103
|
+
if supplied:
|
|
104
|
+
names = ", ".join(sorted(supplied))
|
|
105
|
+
raise TypeError(
|
|
106
|
+
f"TaskArg parameters are supplied by Labtasker and cannot be passed: {names}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def invoke(
|
|
110
|
+
self,
|
|
111
|
+
task_args: dict[str, JSONValue],
|
|
112
|
+
startup_args: tuple[object, ...],
|
|
113
|
+
startup_kwargs: dict[str, object],
|
|
114
|
+
) -> Any:
|
|
115
|
+
bound = self.signature.bind(*startup_args, **startup_kwargs)
|
|
116
|
+
for injection in self.injections:
|
|
117
|
+
bound.arguments[injection.name] = injection.resolve(task_args)
|
|
118
|
+
return self.function(*bound.args, **bound.kwargs)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def compile_binding(function: Callable[..., Any]) -> CompiledBinding:
|
|
122
|
+
if inspect.iscoroutinefunction(function):
|
|
123
|
+
raise TypeError("Labtasker v2 Worker handlers must be synchronous functions.")
|
|
124
|
+
signature = inspect.signature(function)
|
|
125
|
+
try:
|
|
126
|
+
hints = get_type_hints(function, include_extras=True)
|
|
127
|
+
except Exception as error:
|
|
128
|
+
raise TypeError(f"Could not resolve Worker annotations: {error}") from error
|
|
129
|
+
injections: list[_Injection] = []
|
|
130
|
+
for parameter in signature.parameters.values():
|
|
131
|
+
marker = parameter.default
|
|
132
|
+
if not isinstance(marker, _TaskArgMarker):
|
|
133
|
+
continue
|
|
134
|
+
if parameter.kind in {
|
|
135
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
136
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
137
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
138
|
+
}:
|
|
139
|
+
raise TypeError(f"TaskArg parameter {parameter.name!r} must accept a keyword value.")
|
|
140
|
+
if marker.path is not None and not isinstance(marker.path, str):
|
|
141
|
+
raise TypeError(f"TaskArg path for {parameter.name!r} must be a string or None.")
|
|
142
|
+
try:
|
|
143
|
+
path = parse_path(parameter.name if marker.path is None else marker.path)
|
|
144
|
+
except PathError as error:
|
|
145
|
+
raise TypeError(f"Invalid TaskArg path for {parameter.name!r}: {error}") from error
|
|
146
|
+
resolver = marker.resolver
|
|
147
|
+
if resolver is not None:
|
|
148
|
+
_validate_resolver(resolver, parameter.name)
|
|
149
|
+
annotation = hints.get(parameter.name, parameter.annotation)
|
|
150
|
+
adapter: TypeAdapter[Any] | None = None
|
|
151
|
+
if annotation is not inspect.Parameter.empty:
|
|
152
|
+
try:
|
|
153
|
+
adapter = TypeAdapter(annotation)
|
|
154
|
+
except Exception as error:
|
|
155
|
+
raise TypeError(
|
|
156
|
+
f"Could not compile annotation for TaskArg {parameter.name!r}: {error}"
|
|
157
|
+
) from error
|
|
158
|
+
injections.append(
|
|
159
|
+
_Injection(
|
|
160
|
+
name=parameter.name,
|
|
161
|
+
path=path,
|
|
162
|
+
default=marker.default,
|
|
163
|
+
resolver=resolver,
|
|
164
|
+
adapter=adapter,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
return CompiledBinding(function=function, signature=signature, injections=tuple(injections))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _validate_resolver(resolver: object, name: str) -> None:
|
|
171
|
+
if not callable(resolver):
|
|
172
|
+
raise TypeError(f"TaskArg resolver for {name!r} must be callable.")
|
|
173
|
+
if inspect.iscoroutinefunction(resolver):
|
|
174
|
+
raise TypeError(f"TaskArg resolver for {name!r} must be synchronous.")
|
|
175
|
+
try:
|
|
176
|
+
signature = inspect.signature(resolver)
|
|
177
|
+
except (TypeError, ValueError) as error:
|
|
178
|
+
raise TypeError(f"Could not inspect TaskArg resolver for {name!r}.") from error
|
|
179
|
+
parameters = list(signature.parameters.values())
|
|
180
|
+
if len(parameters) != 1 or parameters[0].kind not in {
|
|
181
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
182
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
183
|
+
}:
|
|
184
|
+
raise TypeError(f"TaskArg resolver for {name!r} must accept exactly one value.")
|