fairchild 0.0.4__tar.gz → 0.0.5__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.
- {fairchild-0.0.4 → fairchild-0.0.5}/PKG-INFO +1 -1
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/cli.py +4 -12
- fairchild-0.0.5/fairchild/db/migrations/005_add_worker_tasks.sql +3 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/task.py +46 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/templates/dashboard.html +5 -30
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/ui.py +61 -57
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/worker.py +7 -3
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild.egg-info/PKG-INFO +1 -1
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild.egg-info/SOURCES.txt +1 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/pyproject.toml +1 -1
- {fairchild-0.0.4 → fairchild-0.0.5}/tests/test_web_ui.py +41 -15
- {fairchild-0.0.4 → fairchild-0.0.5}/LICENSE +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/README.md +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/__init__.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/context.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/db/__init__.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/db/migrations/001_initial.sql +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/db/migrations/002_add_parent_id.sql +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/db/migrations/003_remove_workflows.sql +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/db/migrations/004_add_workers.sql +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/db/migrations.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/fairchild.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/future.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/job.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/record.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild/templates/job.html +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild.egg-info/dependency_links.txt +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild.egg-info/entry_points.txt +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild.egg-info/requires.txt +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/fairchild.egg-info/top_level.txt +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/setup.cfg +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/tests/test_cli.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/tests/test_integration.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/tests/test_job.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/tests/test_record.py +0 -0
- {fairchild-0.0.4 → fairchild-0.0.5}/tests/test_task.py +0 -0
|
@@ -162,19 +162,11 @@ async def _run_workers(queue_config: dict[str, int]):
|
|
|
162
162
|
@cli.command()
|
|
163
163
|
@click.option("--host", "-h", default="127.0.0.1", help="Host to bind to")
|
|
164
164
|
@click.option("--port", "-p", default=4000, help="Port to bind to")
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
"-i",
|
|
168
|
-
"imports",
|
|
169
|
-
multiple=True,
|
|
170
|
-
help='Module to import for task registration, e.g., "myapp.tasks"',
|
|
171
|
-
)
|
|
172
|
-
def ui(host: str, port: int, imports: tuple[str, ...]):
|
|
173
|
-
"""Start the web UI dashboard."""
|
|
174
|
-
# Import task modules
|
|
175
|
-
for module_path in imports:
|
|
176
|
-
import_module(module_path)
|
|
165
|
+
def ui(host: str, port: int):
|
|
166
|
+
"""Start the web UI dashboard.
|
|
177
167
|
|
|
168
|
+
The UI reads task definitions from active workers, so no --import is needed.
|
|
169
|
+
"""
|
|
178
170
|
click.echo(f"Starting Fairchild UI at http://{host}:{port}")
|
|
179
171
|
asyncio.run(_run_ui(host, port))
|
|
180
172
|
|
|
@@ -15,6 +15,52 @@ def get_task(name: str) -> "Task":
|
|
|
15
15
|
return _task_registry[name]
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def get_task_schemas() -> list[dict]:
|
|
19
|
+
"""Get schemas for all registered tasks.
|
|
20
|
+
|
|
21
|
+
Returns a list of task info dicts suitable for storing in the database.
|
|
22
|
+
"""
|
|
23
|
+
tasks = []
|
|
24
|
+
for name, task in sorted(_task_registry.items()):
|
|
25
|
+
sig = inspect.signature(task.fn)
|
|
26
|
+
params = []
|
|
27
|
+
for param_name, param in sig.parameters.items():
|
|
28
|
+
if param_name in ("job", "workflow"):
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
param_info = {"name": param_name}
|
|
32
|
+
|
|
33
|
+
if param.annotation != inspect.Parameter.empty:
|
|
34
|
+
try:
|
|
35
|
+
param_info["type"] = param.annotation.__name__
|
|
36
|
+
except AttributeError:
|
|
37
|
+
param_info["type"] = str(param.annotation)
|
|
38
|
+
|
|
39
|
+
if param.default != inspect.Parameter.empty:
|
|
40
|
+
param_info["default"] = param.default
|
|
41
|
+
param_info["required"] = False
|
|
42
|
+
else:
|
|
43
|
+
param_info["required"] = True
|
|
44
|
+
|
|
45
|
+
params.append(param_info)
|
|
46
|
+
|
|
47
|
+
docstring = inspect.getdoc(task.fn)
|
|
48
|
+
|
|
49
|
+
tasks.append(
|
|
50
|
+
{
|
|
51
|
+
"name": name,
|
|
52
|
+
"queue": task.queue,
|
|
53
|
+
"priority": task.priority,
|
|
54
|
+
"max_attempts": task.max_attempts,
|
|
55
|
+
"tags": task.tags,
|
|
56
|
+
"params": params,
|
|
57
|
+
"docstring": docstring,
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return tasks
|
|
62
|
+
|
|
63
|
+
|
|
18
64
|
def task(
|
|
19
65
|
queue: str = "default",
|
|
20
66
|
max_attempts: int = 3,
|
|
@@ -354,13 +354,6 @@
|
|
|
354
354
|
font-weight: 500;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
#workflows-table tbody tr {
|
|
358
|
-
cursor: pointer;
|
|
359
|
-
}
|
|
360
|
-
#workflows-table tbody tr:hover {
|
|
361
|
-
background: var(--bg-hover);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
357
|
/* Enqueue Modal */
|
|
365
358
|
.modal-overlay {
|
|
366
359
|
display: none;
|
|
@@ -1214,25 +1207,6 @@
|
|
|
1214
1207
|
}
|
|
1215
1208
|
}
|
|
1216
1209
|
|
|
1217
|
-
async function fetchWorkflows() {
|
|
1218
|
-
const res = await fetch("/api/workflows");
|
|
1219
|
-
const workflows = await res.json();
|
|
1220
|
-
|
|
1221
|
-
const tbody = document.querySelector("#workflows-table tbody");
|
|
1222
|
-
tbody.innerHTML = workflows
|
|
1223
|
-
.map(
|
|
1224
|
-
(wf) => `
|
|
1225
|
-
<tr onclick="window.location='/workflows/${wf.workflow_id}'">
|
|
1226
|
-
<td>${wf.workflow_name || wf.workflow_id.slice(0, 8)}</td>
|
|
1227
|
-
<td>${wf.completed}/${wf.total_jobs} completed</td>
|
|
1228
|
-
<td class="mono">${formatTime(wf.started_at)}</td>
|
|
1229
|
-
<td class="mono">${wf.finished_at ? formatTime(wf.finished_at) : "-"}</td>
|
|
1230
|
-
</tr>
|
|
1231
|
-
`,
|
|
1232
|
-
)
|
|
1233
|
-
.join("");
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
1210
|
async function fetchJobs(state = "") {
|
|
1237
1211
|
const url = state ? `/api/jobs?state=${state}` : "/api/jobs";
|
|
1238
1212
|
const res = await fetch(url);
|
|
@@ -1462,7 +1436,6 @@
|
|
|
1462
1436
|
fetchStats(),
|
|
1463
1437
|
fetchQueues(),
|
|
1464
1438
|
fetchWorkers(currentWorkerState),
|
|
1465
|
-
fetchWorkflows(),
|
|
1466
1439
|
fetchJobs(currentState),
|
|
1467
1440
|
fetchTimeseries(),
|
|
1468
1441
|
]);
|
|
@@ -1484,14 +1457,17 @@
|
|
|
1484
1457
|
}, 1000);
|
|
1485
1458
|
|
|
1486
1459
|
// Enqueue modal
|
|
1487
|
-
let tasksLoaded = false;
|
|
1488
1460
|
let taskRegistry = {};
|
|
1489
1461
|
|
|
1490
1462
|
async function loadTasks() {
|
|
1491
|
-
if (tasksLoaded) return;
|
|
1492
1463
|
const res = await fetch("/api/tasks");
|
|
1493
1464
|
const tasks = await res.json();
|
|
1494
1465
|
const select = document.getElementById("enqueue-task");
|
|
1466
|
+
|
|
1467
|
+
// Clear existing options except the placeholder
|
|
1468
|
+
select.innerHTML = '<option value="">Select a task...</option>';
|
|
1469
|
+
taskRegistry = {};
|
|
1470
|
+
|
|
1495
1471
|
tasks.forEach((task) => {
|
|
1496
1472
|
taskRegistry[task.name] = task;
|
|
1497
1473
|
const option = document.createElement("option");
|
|
@@ -1499,7 +1475,6 @@
|
|
|
1499
1475
|
option.textContent = `${task.name} (queue: ${task.queue})`;
|
|
1500
1476
|
select.appendChild(option);
|
|
1501
1477
|
});
|
|
1502
|
-
tasksLoaded = true;
|
|
1503
1478
|
}
|
|
1504
1479
|
|
|
1505
1480
|
function onTaskSelected() {
|
|
@@ -96,52 +96,32 @@ async def api_queues(request: web.Request) -> web.Response:
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
async def api_tasks(request: web.Request) -> web.Response:
|
|
99
|
-
"""Get list of registered tasks
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
"""Get list of registered tasks from active workers."""
|
|
100
|
+
fairchild: Fairchild = request.app[_fairchild_key]
|
|
101
|
+
|
|
102
|
+
# Get tasks from workers that have heartbeated recently (alive)
|
|
103
|
+
query = """
|
|
104
|
+
SELECT tasks FROM fairchild_workers
|
|
105
|
+
WHERE last_heartbeat_at > now() - interval '30 seconds'
|
|
106
|
+
AND state != 'stopped'
|
|
107
|
+
"""
|
|
108
|
+
rows = await fairchild._pool.fetch(query)
|
|
102
109
|
|
|
110
|
+
# Aggregate unique tasks from all workers
|
|
111
|
+
seen = set()
|
|
103
112
|
tasks = []
|
|
104
|
-
for
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if param.annotation != inspect.Parameter.empty:
|
|
117
|
-
try:
|
|
118
|
-
param_info["type"] = param.annotation.__name__
|
|
119
|
-
except AttributeError:
|
|
120
|
-
param_info["type"] = str(param.annotation)
|
|
121
|
-
|
|
122
|
-
# Add default value if present
|
|
123
|
-
if param.default != inspect.Parameter.empty:
|
|
124
|
-
param_info["default"] = param.default
|
|
125
|
-
param_info["required"] = False
|
|
126
|
-
else:
|
|
127
|
-
param_info["required"] = True
|
|
128
|
-
|
|
129
|
-
params.append(param_info)
|
|
130
|
-
|
|
131
|
-
# Get docstring
|
|
132
|
-
docstring = inspect.getdoc(task.fn)
|
|
133
|
-
|
|
134
|
-
tasks.append(
|
|
135
|
-
{
|
|
136
|
-
"name": name,
|
|
137
|
-
"queue": task.queue,
|
|
138
|
-
"priority": task.priority,
|
|
139
|
-
"max_attempts": task.max_attempts,
|
|
140
|
-
"tags": task.tags,
|
|
141
|
-
"params": params,
|
|
142
|
-
"docstring": docstring,
|
|
143
|
-
}
|
|
144
|
-
)
|
|
113
|
+
for row in rows:
|
|
114
|
+
worker_tasks = row["tasks"] or []
|
|
115
|
+
# Handle case where tasks is a JSON string (asyncpg returns jsonb as parsed, but just in case)
|
|
116
|
+
if isinstance(worker_tasks, str):
|
|
117
|
+
worker_tasks = json.loads(worker_tasks)
|
|
118
|
+
for task in worker_tasks:
|
|
119
|
+
if task["name"] not in seen:
|
|
120
|
+
seen.add(task["name"])
|
|
121
|
+
tasks.append(task)
|
|
122
|
+
|
|
123
|
+
# Sort by name
|
|
124
|
+
tasks.sort(key=lambda t: t["name"])
|
|
145
125
|
|
|
146
126
|
return web.json_response(tasks)
|
|
147
127
|
|
|
@@ -234,8 +214,8 @@ async def api_enqueue_job(request: web.Request) -> web.Response:
|
|
|
234
214
|
"scheduled_at": "ISO8601"
|
|
235
215
|
}
|
|
236
216
|
"""
|
|
237
|
-
from datetime import datetime
|
|
238
|
-
from fairchild.
|
|
217
|
+
from datetime import datetime, timezone
|
|
218
|
+
from fairchild.job import Job, JobState
|
|
239
219
|
|
|
240
220
|
fairchild: Fairchild = request.app[_fairchild_key]
|
|
241
221
|
|
|
@@ -249,10 +229,27 @@ async def api_enqueue_job(request: web.Request) -> web.Response:
|
|
|
249
229
|
if not task_name:
|
|
250
230
|
return web.json_response({"error": "Missing required field: task"}, status=400)
|
|
251
231
|
|
|
252
|
-
# Look up the task
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
232
|
+
# Look up the task from active workers
|
|
233
|
+
query = """
|
|
234
|
+
SELECT tasks FROM fairchild_workers
|
|
235
|
+
WHERE last_heartbeat_at > now() - interval '30 seconds'
|
|
236
|
+
AND state != 'stopped'
|
|
237
|
+
"""
|
|
238
|
+
rows = await fairchild._pool.fetch(query)
|
|
239
|
+
|
|
240
|
+
task_info = None
|
|
241
|
+
for row in rows:
|
|
242
|
+
worker_tasks = row["tasks"] or []
|
|
243
|
+
if isinstance(worker_tasks, str):
|
|
244
|
+
worker_tasks = json.loads(worker_tasks)
|
|
245
|
+
for t in worker_tasks:
|
|
246
|
+
if t["name"] == task_name:
|
|
247
|
+
task_info = t
|
|
248
|
+
break
|
|
249
|
+
if task_info:
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
if not task_info:
|
|
256
253
|
return web.json_response({"error": f"Unknown task: {task_name}"}, status=404)
|
|
257
254
|
|
|
258
255
|
# Parse optional fields
|
|
@@ -278,14 +275,21 @@ async def api_enqueue_job(request: web.Request) -> web.Response:
|
|
|
278
275
|
{"error": "scheduled_at must be a valid ISO8601 datetime"}, status=400
|
|
279
276
|
)
|
|
280
277
|
|
|
281
|
-
#
|
|
278
|
+
# Create and insert the job directly using task info from workers
|
|
279
|
+
now = datetime.now(timezone.utc)
|
|
280
|
+
job = Job(
|
|
281
|
+
task_name=task_name,
|
|
282
|
+
queue=task_info["queue"],
|
|
283
|
+
args=args,
|
|
284
|
+
priority=priority if priority is not None else task_info["priority"],
|
|
285
|
+
max_attempts=task_info["max_attempts"],
|
|
286
|
+
tags=task_info.get("tags", []),
|
|
287
|
+
scheduled_at=scheduled_at or now,
|
|
288
|
+
state=JobState.AVAILABLE if scheduled_at is None else JobState.SCHEDULED,
|
|
289
|
+
)
|
|
290
|
+
|
|
282
291
|
try:
|
|
283
|
-
|
|
284
|
-
task=task,
|
|
285
|
-
args=args,
|
|
286
|
-
priority=priority,
|
|
287
|
-
scheduled_at=scheduled_at,
|
|
288
|
-
)
|
|
292
|
+
await fairchild._insert_job(job)
|
|
289
293
|
except Exception as e:
|
|
290
294
|
return web.json_response({"error": f"Failed to enqueue job: {e}"}, status=500)
|
|
291
295
|
|
|
@@ -11,7 +11,7 @@ from fairchild.context import set_current_job, get_pending_children
|
|
|
11
11
|
from fairchild.fairchild import Fairchild
|
|
12
12
|
from fairchild.job import Job, JobState
|
|
13
13
|
from fairchild.record import Record
|
|
14
|
-
from fairchild.task import get_task
|
|
14
|
+
from fairchild.task import get_task, get_task_schemas
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Worker:
|
|
@@ -398,26 +398,30 @@ class WorkerPool:
|
|
|
398
398
|
async def _register(self):
|
|
399
399
|
"""Register this worker pool in the database."""
|
|
400
400
|
query = """
|
|
401
|
-
INSERT INTO fairchild_workers (id, hostname, pid, queues, state)
|
|
402
|
-
VALUES ($1, $2, $3, $4::jsonb, 'running')
|
|
401
|
+
INSERT INTO fairchild_workers (id, hostname, pid, queues, tasks, state)
|
|
402
|
+
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, 'running')
|
|
403
403
|
ON CONFLICT (id) DO UPDATE SET
|
|
404
404
|
hostname = EXCLUDED.hostname,
|
|
405
405
|
pid = EXCLUDED.pid,
|
|
406
406
|
queues = EXCLUDED.queues,
|
|
407
|
+
tasks = EXCLUDED.tasks,
|
|
407
408
|
state = 'running',
|
|
408
409
|
last_heartbeat_at = now()
|
|
409
410
|
"""
|
|
410
411
|
# asyncpg requires JSON as a string for jsonb columns
|
|
411
412
|
import json as json_module
|
|
412
413
|
|
|
414
|
+
tasks = get_task_schemas()
|
|
413
415
|
await self.fairchild._pool.execute(
|
|
414
416
|
query,
|
|
415
417
|
self.id,
|
|
416
418
|
self.hostname,
|
|
417
419
|
self.pid,
|
|
418
420
|
json_module.dumps(self.queue_config),
|
|
421
|
+
json_module.dumps(tasks),
|
|
419
422
|
)
|
|
420
423
|
print(f"Registered worker pool {self.id} ({self.hostname}:{self.pid})")
|
|
424
|
+
print(f" Tasks: {[t['name'] for t in tasks]}")
|
|
421
425
|
|
|
422
426
|
async def _heartbeat_loop(self):
|
|
423
427
|
"""Periodically send heartbeats and check for pause state."""
|
|
@@ -23,6 +23,7 @@ fairchild/db/migrations/001_initial.sql
|
|
|
23
23
|
fairchild/db/migrations/002_add_parent_id.sql
|
|
24
24
|
fairchild/db/migrations/003_remove_workflows.sql
|
|
25
25
|
fairchild/db/migrations/004_add_workers.sql
|
|
26
|
+
fairchild/db/migrations/005_add_worker_tasks.sql
|
|
26
27
|
fairchild/templates/dashboard.html
|
|
27
28
|
fairchild/templates/job.html
|
|
28
29
|
tests/test_cli.py
|
|
@@ -50,6 +50,39 @@ async def client(fairchild_app):
|
|
|
50
50
|
yield client
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
@pytest_asyncio.fixture
|
|
54
|
+
async def client_with_worker(fairchild_app):
|
|
55
|
+
"""Create a test client with a simulated worker registration."""
|
|
56
|
+
import json
|
|
57
|
+
from uuid import uuid4
|
|
58
|
+
from fairchild.task import get_task_schemas
|
|
59
|
+
from fairchild.ui import _fairchild_key
|
|
60
|
+
|
|
61
|
+
# Import tasks to register them in _task_registry
|
|
62
|
+
import examples.tasks # noqa: F401
|
|
63
|
+
|
|
64
|
+
fairchild = fairchild_app[_fairchild_key]
|
|
65
|
+
|
|
66
|
+
# Register a fake worker with task schemas (simulates what real workers do)
|
|
67
|
+
worker_id = uuid4()
|
|
68
|
+
tasks = get_task_schemas()
|
|
69
|
+
query = """
|
|
70
|
+
INSERT INTO fairchild_workers (id, hostname, pid, queues, tasks, state)
|
|
71
|
+
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, 'running')
|
|
72
|
+
"""
|
|
73
|
+
await fairchild._pool.execute(
|
|
74
|
+
query,
|
|
75
|
+
worker_id,
|
|
76
|
+
"test-host",
|
|
77
|
+
12345,
|
|
78
|
+
json.dumps({"default": 1}),
|
|
79
|
+
json.dumps(tasks),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async with TestClient(TestServer(fairchild_app)) as client:
|
|
83
|
+
yield client
|
|
84
|
+
|
|
85
|
+
|
|
53
86
|
@pytest.mark.asyncio
|
|
54
87
|
async def test_dashboard_returns_html(client):
|
|
55
88
|
"""Test that the dashboard endpoint returns HTML."""
|
|
@@ -121,12 +154,9 @@ async def test_api_timeseries_returns_data(client):
|
|
|
121
154
|
|
|
122
155
|
|
|
123
156
|
@pytest.mark.asyncio
|
|
124
|
-
async def test_enqueue_job_via_api(
|
|
157
|
+
async def test_enqueue_job_via_api(client_with_worker):
|
|
125
158
|
"""Test enqueuing a job through the JSON API."""
|
|
126
|
-
|
|
127
|
-
import examples.tasks # noqa: F401
|
|
128
|
-
|
|
129
|
-
resp = await client.post(
|
|
159
|
+
resp = await client_with_worker.post(
|
|
130
160
|
"/api/jobs", json={"task": "examples.tasks.add", "args": {"a": 10, "b": 20}}
|
|
131
161
|
)
|
|
132
162
|
assert resp.status == 201
|
|
@@ -137,12 +167,10 @@ async def test_enqueue_job_via_api(client):
|
|
|
137
167
|
|
|
138
168
|
|
|
139
169
|
@pytest.mark.asyncio
|
|
140
|
-
async def test_enqueue_and_fetch_job(
|
|
170
|
+
async def test_enqueue_and_fetch_job(client_with_worker):
|
|
141
171
|
"""Test enqueuing a job and fetching its details."""
|
|
142
|
-
import examples.tasks # noqa: F401
|
|
143
|
-
|
|
144
172
|
# Enqueue a job
|
|
145
|
-
resp = await
|
|
173
|
+
resp = await client_with_worker.post(
|
|
146
174
|
"/api/jobs", json={"task": "examples.tasks.hello", "args": {"name": "TestUser"}}
|
|
147
175
|
)
|
|
148
176
|
assert resp.status == 201
|
|
@@ -150,7 +178,7 @@ async def test_enqueue_and_fetch_job(client):
|
|
|
150
178
|
job_id = enqueue_data["id"]
|
|
151
179
|
|
|
152
180
|
# Fetch the job details
|
|
153
|
-
resp = await
|
|
181
|
+
resp = await client_with_worker.get(f"/api/jobs/{job_id}")
|
|
154
182
|
assert resp.status == 200
|
|
155
183
|
job_data = await resp.json()
|
|
156
184
|
assert job_data["id"] == job_id
|
|
@@ -165,19 +193,17 @@ async def test_enqueue_and_fetch_job(client):
|
|
|
165
193
|
|
|
166
194
|
|
|
167
195
|
@pytest.mark.asyncio
|
|
168
|
-
async def test_job_page_returns_html(
|
|
196
|
+
async def test_job_page_returns_html(client_with_worker):
|
|
169
197
|
"""Test that the job detail page returns HTML."""
|
|
170
|
-
import examples.tasks # noqa: F401
|
|
171
|
-
|
|
172
198
|
# First enqueue a job to get a valid ID
|
|
173
|
-
resp = await
|
|
199
|
+
resp = await client_with_worker.post(
|
|
174
200
|
"/api/jobs", json={"task": "examples.tasks.add", "args": {"a": 1, "b": 2}}
|
|
175
201
|
)
|
|
176
202
|
data = await resp.json()
|
|
177
203
|
job_id = data["id"]
|
|
178
204
|
|
|
179
205
|
# Fetch the job page
|
|
180
|
-
resp = await
|
|
206
|
+
resp = await client_with_worker.get(f"/jobs/{job_id}")
|
|
181
207
|
assert resp.status == 200
|
|
182
208
|
assert resp.content_type == "text/html"
|
|
183
209
|
text = await resp.text()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|