bunqueue-client 0.1.5__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.
- bunqueue/__init__.py +69 -0
- bunqueue/ack_batcher.py +93 -0
- bunqueue/connection.py +240 -0
- bunqueue/connection_lifecycle.py +104 -0
- bunqueue/errors.py +38 -0
- bunqueue/events.py +56 -0
- bunqueue/flow.py +257 -0
- bunqueue/job.py +179 -0
- bunqueue/options.py +126 -0
- bunqueue/py.typed +0 -0
- bunqueue/queue.py +230 -0
- bunqueue/queue_admin.py +201 -0
- bunqueue/queue_query.py +204 -0
- bunqueue/simple/__init__.py +16 -0
- bunqueue/simple/aging.py +68 -0
- bunqueue/simple/app.py +163 -0
- bunqueue/simple/app_api.py +142 -0
- bunqueue/simple/batch.py +88 -0
- bunqueue/simple/cancellation.py +79 -0
- bunqueue/simple/circuit_breaker.py +93 -0
- bunqueue/simple/dedup_debounce.py +42 -0
- bunqueue/simple/rate_gate.py +57 -0
- bunqueue/simple/retry.py +65 -0
- bunqueue/simple/triggers.py +45 -0
- bunqueue/simple/ttl.py +34 -0
- bunqueue/telemetry.py +29 -0
- bunqueue/transport.py +50 -0
- bunqueue/wire.py +99 -0
- bunqueue/worker.py +259 -0
- bunqueue/worker_runtime.py +221 -0
- bunqueue_client-0.1.5.dist-info/METADATA +197 -0
- bunqueue_client-0.1.5.dist-info/RECORD +34 -0
- bunqueue_client-0.1.5.dist-info/WHEEL +4 -0
- bunqueue_client-0.1.5.dist-info/licenses/LICENSE +21 -0
bunqueue/flow.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""FlowProducer: parent/child job trees, chains, and fan-in flows.
|
|
2
|
+
|
|
3
|
+
Mirrors the TS FlowProducer (TCP mode): children are pushed BEFORE their
|
|
4
|
+
parent; the parent carries dependsOn/childrenIds; each child is then linked
|
|
5
|
+
via UpdateParent. On any failure every already-created job is cancelled
|
|
6
|
+
(atomic rollback).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
from .connection import Connection, TlsOption, _compact
|
|
14
|
+
from .errors import CommandError
|
|
15
|
+
from .job import Job
|
|
16
|
+
from .options import job_options, job_payload
|
|
17
|
+
from .telemetry import TelemetryHandler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FlowNode:
|
|
21
|
+
"""Result node: the created job plus its (optional) children nodes."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, job: Job, children: Optional[List["FlowNode"]] = None) -> None:
|
|
24
|
+
self.job = job
|
|
25
|
+
self.children = children or []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FlowProducer:
|
|
29
|
+
"""Create dependent job hierarchies across one or more queues.
|
|
30
|
+
|
|
31
|
+
Flow tree entries: ``{"name", "queueName", "data", "opts", "children": [...]}``
|
|
32
|
+
(same shape as BullMQ / the TS client).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
host: str = "localhost",
|
|
38
|
+
port: int = 6789,
|
|
39
|
+
token: Optional[str] = None,
|
|
40
|
+
tls: TlsOption = None,
|
|
41
|
+
connection: Optional[Connection] = None,
|
|
42
|
+
on_telemetry: Optional[TelemetryHandler] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.connection = connection or Connection(
|
|
45
|
+
host=host, port=port, token=token, tls=tls, on_telemetry=on_telemetry
|
|
46
|
+
)
|
|
47
|
+
self._owns_connection = connection is None
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------------- api
|
|
50
|
+
|
|
51
|
+
def add(self, flow: Dict[str, Any], opts: Optional[Dict[str, Any]] = None) -> FlowNode:
|
|
52
|
+
"""Add a flow tree. Children are processed BEFORE their parent."""
|
|
53
|
+
created: List[str] = []
|
|
54
|
+
try:
|
|
55
|
+
return self._add_node(flow, None, created, opts or {})
|
|
56
|
+
except Exception:
|
|
57
|
+
self._cleanup(created)
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
def add_bulk(self, flows: Sequence[Dict[str, Any]]) -> List[FlowNode]:
|
|
61
|
+
results: List[FlowNode] = []
|
|
62
|
+
created: List[str] = []
|
|
63
|
+
try:
|
|
64
|
+
for flow in flows:
|
|
65
|
+
results.append(self._add_node(flow, None, created, {}))
|
|
66
|
+
return results
|
|
67
|
+
except Exception:
|
|
68
|
+
self._cleanup(created)
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
def add_chain(self, steps: Sequence[Dict[str, Any]]) -> List[str]:
|
|
72
|
+
"""Sequential chain: step[0] -> step[1] -> ... (each depends on the previous)."""
|
|
73
|
+
job_ids: List[str] = []
|
|
74
|
+
prev_id: Optional[str] = None
|
|
75
|
+
try:
|
|
76
|
+
for step in steps:
|
|
77
|
+
data = {"__flowParentId": prev_id, **(step.get("data") or {})}
|
|
78
|
+
job_id = self._push(
|
|
79
|
+
step["queueName"],
|
|
80
|
+
job_payload(step["name"], data),
|
|
81
|
+
step.get("opts") or {},
|
|
82
|
+
depends_on=[prev_id] if prev_id else None,
|
|
83
|
+
)
|
|
84
|
+
job_ids.append(job_id)
|
|
85
|
+
prev_id = job_id
|
|
86
|
+
return job_ids
|
|
87
|
+
except Exception:
|
|
88
|
+
self._cleanup(job_ids)
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
def add_bulk_then(
|
|
92
|
+
self, parallel: Sequence[Dict[str, Any]], final: Dict[str, Any]
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
"""Fan-in: N independent jobs converging into one final job.
|
|
95
|
+
|
|
96
|
+
The final job records the parallel ids as childrenIds so its processor
|
|
97
|
+
can read their results via ``get_children_values``.
|
|
98
|
+
"""
|
|
99
|
+
parallel_ids: List[str] = []
|
|
100
|
+
try:
|
|
101
|
+
for step in parallel:
|
|
102
|
+
parallel_ids.append(
|
|
103
|
+
self._push(
|
|
104
|
+
step["queueName"],
|
|
105
|
+
job_payload(step["name"], step.get("data")),
|
|
106
|
+
step.get("opts") or {},
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
final_data = {"__flowParentIds": parallel_ids, **(final.get("data") or {})}
|
|
110
|
+
final_id = self._push_with_children(
|
|
111
|
+
final["queueName"],
|
|
112
|
+
job_payload(final["name"], final_data),
|
|
113
|
+
final.get("opts") or {},
|
|
114
|
+
child_ids=parallel_ids,
|
|
115
|
+
parent_id=None,
|
|
116
|
+
)
|
|
117
|
+
return {"parallel_ids": parallel_ids, "final_id": final_id}
|
|
118
|
+
except Exception:
|
|
119
|
+
self._cleanup(parallel_ids)
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
def get_flow(self, job_id: str, depth: Optional[int] = None) -> Optional[FlowNode]:
|
|
123
|
+
"""Reconstruct a flow tree from a root job id (GetJob recursion).
|
|
124
|
+
|
|
125
|
+
``depth`` limits recursion; ``None`` means unlimited (parity with the
|
|
126
|
+
TS client's ``Number.POSITIVE_INFINITY`` default). A missing job — the
|
|
127
|
+
root or any child removed via removeOnComplete/cancel — yields ``None``
|
|
128
|
+
and is skipped, returning the surviving partial tree instead of raising.
|
|
129
|
+
A ``visited`` set guards against cycles in ``childrenIds`` so unlimited
|
|
130
|
+
depth cannot recurse forever.
|
|
131
|
+
"""
|
|
132
|
+
return self._get_flow(job_id, depth, set())
|
|
133
|
+
|
|
134
|
+
def _get_flow(
|
|
135
|
+
self, job_id: str, depth: Optional[int], visited: set
|
|
136
|
+
) -> Optional[FlowNode]:
|
|
137
|
+
if job_id in visited:
|
|
138
|
+
return None # cycle guard: childrenIds already on the current path
|
|
139
|
+
visited.add(job_id)
|
|
140
|
+
try:
|
|
141
|
+
response = self.connection.call({"cmd": "GetJob", "id": job_id})
|
|
142
|
+
except CommandError as exc:
|
|
143
|
+
if "not found" in str(exc).lower():
|
|
144
|
+
return None # root or a since-removed child -> skip
|
|
145
|
+
raise # a real server error must not masquerade as a missing child
|
|
146
|
+
raw = response.get("job")
|
|
147
|
+
if not raw:
|
|
148
|
+
return None
|
|
149
|
+
job = Job(raw, self.connection)
|
|
150
|
+
children: List[FlowNode] = []
|
|
151
|
+
if depth is None or depth > 0:
|
|
152
|
+
next_depth = None if depth is None else depth - 1
|
|
153
|
+
for child_id in job.children_ids:
|
|
154
|
+
child = self._get_flow(child_id, next_depth, visited)
|
|
155
|
+
if child:
|
|
156
|
+
children.append(child)
|
|
157
|
+
return FlowNode(job, children)
|
|
158
|
+
|
|
159
|
+
def close(self) -> None:
|
|
160
|
+
if self._owns_connection:
|
|
161
|
+
self.connection.close()
|
|
162
|
+
|
|
163
|
+
def __enter__(self) -> "FlowProducer":
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def __exit__(self, *exc: Any) -> None:
|
|
167
|
+
self.close()
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------- internals
|
|
170
|
+
|
|
171
|
+
def _add_node(
|
|
172
|
+
self,
|
|
173
|
+
node: Dict[str, Any],
|
|
174
|
+
parent_ref: Optional[Dict[str, str]],
|
|
175
|
+
created: List[str],
|
|
176
|
+
flow_opts: Dict[str, Any],
|
|
177
|
+
) -> FlowNode:
|
|
178
|
+
child_nodes: List[FlowNode] = []
|
|
179
|
+
child_ids: List[str] = []
|
|
180
|
+
|
|
181
|
+
for child in node.get("children") or []:
|
|
182
|
+
child_node = self._add_node(
|
|
183
|
+
child, {"id": "pending", "queue": node["queueName"]}, created, flow_opts
|
|
184
|
+
)
|
|
185
|
+
child_nodes.append(child_node)
|
|
186
|
+
child_ids.append(child_node.job.id)
|
|
187
|
+
|
|
188
|
+
queue_defaults = (flow_opts.get("queues_options") or {}).get(node["queueName"]) or {}
|
|
189
|
+
merged_opts = {**queue_defaults, **(node.get("opts") or {})}
|
|
190
|
+
|
|
191
|
+
data: Dict[str, Any] = dict(node.get("data") or {})
|
|
192
|
+
if parent_ref:
|
|
193
|
+
data["__parentId"] = parent_ref["id"]
|
|
194
|
+
data["__parentQueue"] = parent_ref["queue"]
|
|
195
|
+
if child_ids:
|
|
196
|
+
data["__childrenIds"] = child_ids
|
|
197
|
+
|
|
198
|
+
job_id = self._push_with_children(
|
|
199
|
+
node["queueName"],
|
|
200
|
+
job_payload(node["name"], data),
|
|
201
|
+
merged_opts,
|
|
202
|
+
child_ids=child_ids,
|
|
203
|
+
parent_id=parent_ref["id"] if parent_ref else None,
|
|
204
|
+
)
|
|
205
|
+
created.append(job_id)
|
|
206
|
+
|
|
207
|
+
job = Job({"id": job_id, "queue": node["queueName"], "data": data}, self.connection)
|
|
208
|
+
return FlowNode(job, child_nodes)
|
|
209
|
+
|
|
210
|
+
def _push(
|
|
211
|
+
self,
|
|
212
|
+
queue: str,
|
|
213
|
+
data: Dict[str, Any],
|
|
214
|
+
opts: Dict[str, Any],
|
|
215
|
+
depends_on: Optional[List[str]] = None,
|
|
216
|
+
) -> str:
|
|
217
|
+
command = {
|
|
218
|
+
"cmd": "PUSH",
|
|
219
|
+
"queue": queue,
|
|
220
|
+
"data": data,
|
|
221
|
+
**job_options(**opts),
|
|
222
|
+
}
|
|
223
|
+
if depends_on:
|
|
224
|
+
command["dependsOn"] = depends_on
|
|
225
|
+
response = self.connection.call(_compact(command))
|
|
226
|
+
return str(response.get("id"))
|
|
227
|
+
|
|
228
|
+
def _push_with_children(
|
|
229
|
+
self,
|
|
230
|
+
queue: str,
|
|
231
|
+
data: Dict[str, Any],
|
|
232
|
+
opts: Dict[str, Any],
|
|
233
|
+
child_ids: List[str],
|
|
234
|
+
parent_id: Optional[str],
|
|
235
|
+
) -> str:
|
|
236
|
+
command = {
|
|
237
|
+
"cmd": "PUSH",
|
|
238
|
+
"queue": queue,
|
|
239
|
+
"data": data,
|
|
240
|
+
**job_options(**opts),
|
|
241
|
+
"parentId": parent_id,
|
|
242
|
+
"childrenIds": child_ids or None,
|
|
243
|
+
"dependsOn": child_ids or None,
|
|
244
|
+
}
|
|
245
|
+
response = self.connection.call(_compact(command))
|
|
246
|
+
job_id = str(response.get("id"))
|
|
247
|
+
# Link children to their real parent id (placeholder was 'pending')
|
|
248
|
+
for child_id in child_ids:
|
|
249
|
+
self.connection.call({"cmd": "UpdateParent", "childId": child_id, "parentId": job_id})
|
|
250
|
+
return job_id
|
|
251
|
+
|
|
252
|
+
def _cleanup(self, job_ids: List[str]) -> None:
|
|
253
|
+
for job_id in job_ids:
|
|
254
|
+
try:
|
|
255
|
+
self.connection.call({"cmd": "Cancel", "id": job_id})
|
|
256
|
+
except Exception: # noqa: BLE001 - best-effort rollback
|
|
257
|
+
pass
|
bunqueue/job.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Job wrapper: read view over the server job record + per-job operations.
|
|
2
|
+
|
|
3
|
+
Parity with the TS client's Job object (TCP mode): progress, logs, lock
|
|
4
|
+
management, state transitions, children values.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from .connection import Connection, _compact
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Job:
|
|
15
|
+
"""A job as seen by producers (from queries) or workers (with lock token)."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
raw: Dict[str, Any],
|
|
20
|
+
connection: Optional[Connection] = None,
|
|
21
|
+
token: Optional[str] = None,
|
|
22
|
+
on_progress: Optional[Callable[["Job", Any], None]] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.raw = raw
|
|
25
|
+
self.token = token
|
|
26
|
+
self._conn = connection
|
|
27
|
+
self._on_progress = on_progress
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------ properties
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def id(self) -> str:
|
|
33
|
+
return str(self.raw.get("id"))
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def queue(self) -> str:
|
|
37
|
+
return str(self.raw.get("queue"))
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def data(self) -> Any:
|
|
41
|
+
return self.raw.get("data")
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> Optional[str]:
|
|
45
|
+
"""Job name (stored inside ``data.name``, mirroring the JS SDK)."""
|
|
46
|
+
data = self.raw.get("data")
|
|
47
|
+
if isinstance(data, dict) and data.get("name") is not None:
|
|
48
|
+
return str(data["name"])
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def priority(self) -> int:
|
|
53
|
+
return int(self.raw.get("priority") or 0)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def attempts(self) -> int:
|
|
57
|
+
return int(self.raw.get("attempts") or 0)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def max_attempts(self) -> int:
|
|
61
|
+
return int(self.raw.get("maxAttempts") or 0)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def created_at(self) -> Optional[int]:
|
|
65
|
+
value = self.raw.get("createdAt")
|
|
66
|
+
return int(value) if value is not None else None
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def started_at(self) -> Optional[int]:
|
|
70
|
+
value = self.raw.get("startedAt")
|
|
71
|
+
return int(value) if value is not None else None
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def completed_at(self) -> Optional[int]:
|
|
75
|
+
value = self.raw.get("completedAt")
|
|
76
|
+
return int(value) if value is not None else None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def custom_id(self) -> Optional[str]:
|
|
80
|
+
value = self.raw.get("customId")
|
|
81
|
+
return str(value) if value is not None else None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def tags(self) -> List[str]:
|
|
85
|
+
return list(self.raw.get("tags") or [])
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def group_id(self) -> Optional[str]:
|
|
89
|
+
value = self.raw.get("groupId")
|
|
90
|
+
return str(value) if value is not None else None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def parent_id(self) -> Optional[str]:
|
|
94
|
+
value = self.raw.get("parentId")
|
|
95
|
+
return str(value) if value is not None else None
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def children_ids(self) -> List[str]:
|
|
99
|
+
return [str(c) for c in (self.raw.get("childrenIds") or [])]
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def depends_on(self) -> List[str]:
|
|
103
|
+
return [str(c) for c in (self.raw.get("dependsOn") or [])]
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def progress(self) -> int:
|
|
107
|
+
return int(self.raw.get("progress") or 0)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def stacktrace(self) -> Optional[List[str]]:
|
|
111
|
+
value = self.raw.get("stacktrace")
|
|
112
|
+
return list(value) if value else None
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def state(self) -> Optional[str]:
|
|
116
|
+
value = self.raw.get("state")
|
|
117
|
+
return str(value) if value is not None else None
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------ operations
|
|
120
|
+
|
|
121
|
+
def _call(self, command: Dict[str, Any]) -> Dict[str, Any]:
|
|
122
|
+
if self._conn is None:
|
|
123
|
+
raise RuntimeError("Job is not bound to a connection")
|
|
124
|
+
return self._conn.call(_compact(command))
|
|
125
|
+
|
|
126
|
+
def update_progress(self, progress: int, message: Optional[str] = None) -> None:
|
|
127
|
+
"""Report processing progress (0-100) to the server."""
|
|
128
|
+
self._call({"cmd": "Progress", "id": self.id, "progress": progress, "message": message})
|
|
129
|
+
if self._on_progress is not None:
|
|
130
|
+
self._on_progress(self, progress)
|
|
131
|
+
|
|
132
|
+
def log(self, message: str, level: Optional[str] = None) -> None:
|
|
133
|
+
"""Append a log row to the job (AddLog)."""
|
|
134
|
+
self._call({"cmd": "AddLog", "id": self.id, "message": message, "level": level})
|
|
135
|
+
|
|
136
|
+
def heartbeat(self) -> None:
|
|
137
|
+
"""Renew this job's lock (stall detection)."""
|
|
138
|
+
self._call({"cmd": "JobHeartbeat", "id": self.id, "token": self.token})
|
|
139
|
+
|
|
140
|
+
def extend_lock(self, duration_ms: int) -> None:
|
|
141
|
+
self._call({"cmd": "ExtendLock", "id": self.id, "token": self.token, "duration": duration_ms})
|
|
142
|
+
|
|
143
|
+
def get_state(self) -> str:
|
|
144
|
+
return str(self._call({"cmd": "GetState", "id": self.id}).get("state"))
|
|
145
|
+
|
|
146
|
+
def get_children_values(self) -> Dict[str, Any]:
|
|
147
|
+
response = self._call({"cmd": "GetChildrenValues", "id": self.id})
|
|
148
|
+
data = response.get("data")
|
|
149
|
+
if isinstance(data, dict) and "values" in data:
|
|
150
|
+
return dict(data.get("values") or {})
|
|
151
|
+
return dict(response.get("values") or {})
|
|
152
|
+
|
|
153
|
+
def remove(self) -> None:
|
|
154
|
+
self._call({"cmd": "Cancel", "id": self.id})
|
|
155
|
+
|
|
156
|
+
def discard(self) -> None:
|
|
157
|
+
self._call({"cmd": "Discard", "id": self.id})
|
|
158
|
+
|
|
159
|
+
def promote(self) -> None:
|
|
160
|
+
self._call({"cmd": "Promote", "id": self.id})
|
|
161
|
+
|
|
162
|
+
def retry(self) -> None:
|
|
163
|
+
"""failed -> waiting (BullMQ contract; MoveToWait over TCP)."""
|
|
164
|
+
self._call({"cmd": "MoveToWait", "id": self.id})
|
|
165
|
+
|
|
166
|
+
def update_data(self, data: Any) -> None:
|
|
167
|
+
self._call({"cmd": "Update", "id": self.id, "data": data})
|
|
168
|
+
|
|
169
|
+
def change_priority(self, priority: int, lifo: Optional[bool] = None) -> None:
|
|
170
|
+
self._call({"cmd": "ChangePriority", "id": self.id, "priority": priority, "lifo": lifo})
|
|
171
|
+
|
|
172
|
+
def change_delay(self, delay_ms: int) -> None:
|
|
173
|
+
self._call({"cmd": "ChangeDelay", "id": self.id, "delay": delay_ms})
|
|
174
|
+
|
|
175
|
+
def move_to_delayed(self, delay_ms: int) -> None:
|
|
176
|
+
self._call({"cmd": "MoveToDelayed", "id": self.id, "delay": delay_ms})
|
|
177
|
+
|
|
178
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
179
|
+
return f"Job(id={self.id!r}, queue={self.queue!r}, name={self.name!r})"
|
bunqueue/options.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Job option mapping: pythonic kwargs -> wire fields (parity with TS client)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
from .connection import _compact
|
|
8
|
+
|
|
9
|
+
Backoff = Union[int, Dict[str, Any]] # ms or {"type": "fixed"|"exponential", "delay", "maxDelay"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def job_payload(name: str, data: Any) -> Dict[str, Any]:
|
|
13
|
+
"""Mirror the JS SDK: the job name travels inside ``data``."""
|
|
14
|
+
if data is None:
|
|
15
|
+
return {"name": name}
|
|
16
|
+
if isinstance(data, dict):
|
|
17
|
+
return {"name": name, **data}
|
|
18
|
+
return {"name": name, "payload": data}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_cron_job_options(job_opts: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
22
|
+
"""Map a cron template's Pythonic job options to the wire ``CronJobOptions``.
|
|
23
|
+
|
|
24
|
+
Mirrors the TS reference ``buildCronJobOptions`` (issue #86): the server
|
|
25
|
+
reads only camelCase keys (``maxAttempts``/``removeOnComplete``/…), so
|
|
26
|
+
SDK-native names (``attempts``/``remove_on_complete``/…) must be renamed
|
|
27
|
+
or the cron-spawned jobs silently fall back to JOB_DEFAULTS. Returns
|
|
28
|
+
``None`` when nothing relevant is set (server keeps its own defaults).
|
|
29
|
+
"""
|
|
30
|
+
if not job_opts:
|
|
31
|
+
return None
|
|
32
|
+
out: Dict[str, Any] = {}
|
|
33
|
+
if job_opts.get("attempts") is not None:
|
|
34
|
+
out["maxAttempts"] = job_opts["attempts"]
|
|
35
|
+
if job_opts.get("backoff") is not None:
|
|
36
|
+
out["backoff"] = job_opts["backoff"]
|
|
37
|
+
if job_opts.get("timeout") is not None:
|
|
38
|
+
out["timeout"] = job_opts["timeout"]
|
|
39
|
+
if job_opts.get("delay") is not None:
|
|
40
|
+
out["delay"] = job_opts["delay"]
|
|
41
|
+
if job_opts.get("stall_timeout") is not None:
|
|
42
|
+
out["stallTimeout"] = job_opts["stall_timeout"]
|
|
43
|
+
if isinstance(job_opts.get("remove_on_complete"), bool):
|
|
44
|
+
out["removeOnComplete"] = job_opts["remove_on_complete"]
|
|
45
|
+
if isinstance(job_opts.get("remove_on_fail"), bool):
|
|
46
|
+
out["removeOnFail"] = job_opts["remove_on_fail"]
|
|
47
|
+
return out or None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def job_options(
|
|
51
|
+
*,
|
|
52
|
+
priority: Optional[int] = None,
|
|
53
|
+
delay: Optional[int] = None,
|
|
54
|
+
attempts: Optional[int] = None,
|
|
55
|
+
backoff: Optional[Backoff] = None,
|
|
56
|
+
ttl: Optional[int] = None,
|
|
57
|
+
timeout: Optional[int] = None,
|
|
58
|
+
job_id: Optional[str] = None,
|
|
59
|
+
unique_key: Optional[str] = None,
|
|
60
|
+
deduplication: Optional[Dict[str, Any]] = None, # {"id", "ttl", "extend", "replace"}
|
|
61
|
+
depends_on: Optional[Sequence[str]] = None,
|
|
62
|
+
parent_id: Optional[str] = None,
|
|
63
|
+
children_ids: Optional[Sequence[str]] = None,
|
|
64
|
+
tags: Optional[Sequence[str]] = None,
|
|
65
|
+
group_id: Optional[str] = None,
|
|
66
|
+
lifo: Optional[bool] = None,
|
|
67
|
+
remove_on_complete: Optional[bool] = None,
|
|
68
|
+
remove_on_fail: Optional[bool] = None,
|
|
69
|
+
stall_timeout: Optional[int] = None,
|
|
70
|
+
durable: Optional[bool] = None,
|
|
71
|
+
repeat: Optional[Dict[str, Any]] = None, # {"every", "limit", "pattern", ...} camelCase wire keys
|
|
72
|
+
debounce: Optional[Dict[str, Any]] = None, # {"id", "ttl"}
|
|
73
|
+
stack_trace_limit: Optional[int] = None,
|
|
74
|
+
keep_logs: Optional[int] = None,
|
|
75
|
+
size_limit: Optional[int] = None,
|
|
76
|
+
timestamp: Optional[int] = None,
|
|
77
|
+
fail_parent_on_failure: Optional[bool] = None,
|
|
78
|
+
remove_dependency_on_failure: Optional[bool] = None,
|
|
79
|
+
ignore_dependency_on_failure: Optional[bool] = None,
|
|
80
|
+
continue_parent_on_failure: Optional[bool] = None,
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""Translate SDK options into the TCP PUSH field set (see buildPushPayload #88/#90)."""
|
|
83
|
+
dedup = None
|
|
84
|
+
if deduplication:
|
|
85
|
+
unique_key = unique_key or deduplication.get("id")
|
|
86
|
+
dedup = _compact(
|
|
87
|
+
{
|
|
88
|
+
"ttl": deduplication.get("ttl"),
|
|
89
|
+
"extend": deduplication.get("extend"),
|
|
90
|
+
"replace": deduplication.get("replace"),
|
|
91
|
+
}
|
|
92
|
+
) or None
|
|
93
|
+
return _compact(
|
|
94
|
+
{
|
|
95
|
+
"priority": priority,
|
|
96
|
+
"delay": delay,
|
|
97
|
+
"maxAttempts": attempts,
|
|
98
|
+
"backoff": backoff,
|
|
99
|
+
"ttl": ttl,
|
|
100
|
+
"timeout": timeout,
|
|
101
|
+
"jobId": job_id,
|
|
102
|
+
"uniqueKey": unique_key,
|
|
103
|
+
"dedup": dedup,
|
|
104
|
+
"dependsOn": list(depends_on) if depends_on else None,
|
|
105
|
+
"parentId": parent_id,
|
|
106
|
+
"childrenIds": list(children_ids) if children_ids else None,
|
|
107
|
+
"tags": list(tags) if tags else None,
|
|
108
|
+
"groupId": group_id,
|
|
109
|
+
"lifo": lifo,
|
|
110
|
+
"removeOnComplete": remove_on_complete,
|
|
111
|
+
"removeOnFail": remove_on_fail,
|
|
112
|
+
"stallTimeout": stall_timeout,
|
|
113
|
+
"durable": durable,
|
|
114
|
+
"repeat": repeat,
|
|
115
|
+
"debounceId": debounce.get("id") if debounce else None,
|
|
116
|
+
"debounceTtl": debounce.get("ttl") if debounce else None,
|
|
117
|
+
"stackTraceLimit": stack_trace_limit,
|
|
118
|
+
"keepLogs": keep_logs,
|
|
119
|
+
"sizeLimit": size_limit,
|
|
120
|
+
"timestamp": timestamp,
|
|
121
|
+
"failParentOnFailure": fail_parent_on_failure,
|
|
122
|
+
"removeDependencyOnFailure": remove_dependency_on_failure,
|
|
123
|
+
"ignoreDependencyOnFailure": ignore_dependency_on_failure,
|
|
124
|
+
"continueParentOnFailure": continue_parent_on_failure,
|
|
125
|
+
}
|
|
126
|
+
)
|
bunqueue/py.typed
ADDED
|
File without changes
|