fairchild 0.0.4__py3-none-any.whl → 0.0.6__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.
- fairchild/__init__.py +0 -2
- fairchild/cli.py +4 -12
- fairchild/db/migrations/005_add_worker_tasks.sql +3 -0
- fairchild/db/migrations/006_rename_recorded_to_result.sql +2 -0
- fairchild/task.py +48 -8
- fairchild/templates/dashboard.html +32 -33
- fairchild/templates/job.html +227 -10
- fairchild/ui.py +68 -63
- fairchild/worker.py +30 -22
- {fairchild-0.0.4.dist-info → fairchild-0.0.6.dist-info}/METADATA +1 -1
- fairchild-0.0.6.dist-info/RECORD +25 -0
- fairchild/record.py +0 -22
- fairchild-0.0.4.dist-info/RECORD +0 -24
- {fairchild-0.0.4.dist-info → fairchild-0.0.6.dist-info}/WHEEL +0 -0
- {fairchild-0.0.4.dist-info → fairchild-0.0.6.dist-info}/entry_points.txt +0 -0
- {fairchild-0.0.4.dist-info → fairchild-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {fairchild-0.0.4.dist-info → fairchild-0.0.6.dist-info}/top_level.txt +0 -0
fairchild/__init__.py
CHANGED
fairchild/cli.py
CHANGED
|
@@ -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
|
|
fairchild/task.py
CHANGED
|
@@ -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,
|
|
@@ -28,7 +74,7 @@ def task(
|
|
|
28
74
|
Usage:
|
|
29
75
|
@task(queue="default")
|
|
30
76
|
def my_task(item_id: int):
|
|
31
|
-
return
|
|
77
|
+
return {"result": item_id * 2}
|
|
32
78
|
|
|
33
79
|
# Enqueue
|
|
34
80
|
my_task.enqueue(item_id=42)
|
|
@@ -151,13 +197,7 @@ class Task:
|
|
|
151
197
|
return Future(job_id=job_id)
|
|
152
198
|
|
|
153
199
|
# Not inside a task - execute directly
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
result = self.fn(*args, **kwargs)
|
|
157
|
-
# Unwrap Record so local runs behave like resolved futures
|
|
158
|
-
if isinstance(result, Record):
|
|
159
|
-
return result.value
|
|
160
|
-
return result
|
|
200
|
+
return self.fn(*args, **kwargs)
|
|
161
201
|
|
|
162
202
|
def _serialize_args(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
163
203
|
"""Serialize arguments, converting Futures to their job IDs."""
|
|
@@ -255,6 +255,16 @@
|
|
|
255
255
|
.job-link:hover {
|
|
256
256
|
text-decoration: underline;
|
|
257
257
|
}
|
|
258
|
+
.task-args {
|
|
259
|
+
font-family: "SF Mono", Monaco, monospace;
|
|
260
|
+
font-size: 0.75rem;
|
|
261
|
+
color: var(--text-dim);
|
|
262
|
+
margin-top: 0.25rem;
|
|
263
|
+
max-width: 300px;
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
text-overflow: ellipsis;
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
}
|
|
258
268
|
|
|
259
269
|
.queues {
|
|
260
270
|
display: grid;
|
|
@@ -354,13 +364,6 @@
|
|
|
354
364
|
font-weight: 500;
|
|
355
365
|
}
|
|
356
366
|
|
|
357
|
-
#workflows-table tbody tr {
|
|
358
|
-
cursor: pointer;
|
|
359
|
-
}
|
|
360
|
-
#workflows-table tbody tr:hover {
|
|
361
|
-
background: var(--bg-hover);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
367
|
/* Enqueue Modal */
|
|
365
368
|
.modal-overlay {
|
|
366
369
|
display: none;
|
|
@@ -1214,23 +1217,10 @@
|
|
|
1214
1217
|
}
|
|
1215
1218
|
}
|
|
1216
1219
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
const
|
|
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("");
|
|
1220
|
+
function formatArgs(args) {
|
|
1221
|
+
if (!args || Object.keys(args).length === 0) return "";
|
|
1222
|
+
const str = typeof args === "string" ? args : JSON.stringify(args);
|
|
1223
|
+
return str.length > 50 ? str.slice(0, 50) + "..." : str;
|
|
1234
1224
|
}
|
|
1235
1225
|
|
|
1236
1226
|
async function fetchJobs(state = "") {
|
|
@@ -1240,18 +1230,26 @@
|
|
|
1240
1230
|
|
|
1241
1231
|
const tbody = document.querySelector("#jobs-table tbody");
|
|
1242
1232
|
tbody.innerHTML = jobs
|
|
1243
|
-
.map(
|
|
1244
|
-
(job)
|
|
1233
|
+
.map((job) => {
|
|
1234
|
+
const argsDisplay = formatArgs(job.args);
|
|
1235
|
+
const argsTitle =
|
|
1236
|
+
typeof job.args === "string"
|
|
1237
|
+
? job.args
|
|
1238
|
+
: JSON.stringify(job.args);
|
|
1239
|
+
const argsHtml = argsDisplay
|
|
1240
|
+
? `<div class="task-args" title="${argsTitle.replace(/"/g, """)}">${argsDisplay}</div>`
|
|
1241
|
+
: "";
|
|
1242
|
+
return `
|
|
1245
1243
|
<tr>
|
|
1246
1244
|
<td class="mono"><a href="/jobs/${job.id}" class="job-link">${job.id}</a></td>
|
|
1247
|
-
<td
|
|
1245
|
+
<td>${job.task_name}${argsHtml}</td>
|
|
1248
1246
|
<td>${job.queue}</td>
|
|
1249
1247
|
<td><span class="badge ${job.state}">${job.state}</span></td>
|
|
1250
1248
|
<td>${job.attempt}/${job.max_attempts}</td>
|
|
1251
1249
|
<td class="mono">${formatTime(job.inserted_at)}</td>
|
|
1252
1250
|
</tr>
|
|
1253
|
-
|
|
1254
|
-
)
|
|
1251
|
+
`;
|
|
1252
|
+
})
|
|
1255
1253
|
.join("");
|
|
1256
1254
|
}
|
|
1257
1255
|
|
|
@@ -1462,7 +1460,6 @@
|
|
|
1462
1460
|
fetchStats(),
|
|
1463
1461
|
fetchQueues(),
|
|
1464
1462
|
fetchWorkers(currentWorkerState),
|
|
1465
|
-
fetchWorkflows(),
|
|
1466
1463
|
fetchJobs(currentState),
|
|
1467
1464
|
fetchTimeseries(),
|
|
1468
1465
|
]);
|
|
@@ -1484,14 +1481,17 @@
|
|
|
1484
1481
|
}, 1000);
|
|
1485
1482
|
|
|
1486
1483
|
// Enqueue modal
|
|
1487
|
-
let tasksLoaded = false;
|
|
1488
1484
|
let taskRegistry = {};
|
|
1489
1485
|
|
|
1490
1486
|
async function loadTasks() {
|
|
1491
|
-
if (tasksLoaded) return;
|
|
1492
1487
|
const res = await fetch("/api/tasks");
|
|
1493
1488
|
const tasks = await res.json();
|
|
1494
1489
|
const select = document.getElementById("enqueue-task");
|
|
1490
|
+
|
|
1491
|
+
// Clear existing options except the placeholder
|
|
1492
|
+
select.innerHTML = '<option value="">Select a task...</option>';
|
|
1493
|
+
taskRegistry = {};
|
|
1494
|
+
|
|
1495
1495
|
tasks.forEach((task) => {
|
|
1496
1496
|
taskRegistry[task.name] = task;
|
|
1497
1497
|
const option = document.createElement("option");
|
|
@@ -1499,7 +1499,6 @@
|
|
|
1499
1499
|
option.textContent = `${task.name} (queue: ${task.queue})`;
|
|
1500
1500
|
select.appendChild(option);
|
|
1501
1501
|
});
|
|
1502
|
-
tasksLoaded = true;
|
|
1503
1502
|
}
|
|
1504
1503
|
|
|
1505
1504
|
function onTaskSelected() {
|
fairchild/templates/job.html
CHANGED
|
@@ -392,6 +392,64 @@
|
|
|
392
392
|
border-radius: 4px;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
/* Task Summary Stats */
|
|
396
|
+
.task-summary {
|
|
397
|
+
margin-bottom: 1rem;
|
|
398
|
+
overflow-x: auto;
|
|
399
|
+
}
|
|
400
|
+
.task-summary table {
|
|
401
|
+
width: 100%;
|
|
402
|
+
border-collapse: collapse;
|
|
403
|
+
font-size: 0.8125rem;
|
|
404
|
+
}
|
|
405
|
+
.task-summary th,
|
|
406
|
+
.task-summary td {
|
|
407
|
+
padding: 0.5rem 0.75rem;
|
|
408
|
+
text-align: left;
|
|
409
|
+
border-bottom: 1px solid var(--border-color);
|
|
410
|
+
}
|
|
411
|
+
.task-summary th {
|
|
412
|
+
background: var(--bg-secondary);
|
|
413
|
+
color: var(--text-muted);
|
|
414
|
+
font-weight: 500;
|
|
415
|
+
text-transform: uppercase;
|
|
416
|
+
font-size: 0.6875rem;
|
|
417
|
+
}
|
|
418
|
+
.task-summary td {
|
|
419
|
+
color: var(--text-secondary);
|
|
420
|
+
}
|
|
421
|
+
.task-summary .task-name {
|
|
422
|
+
font-family: "SF Mono", Monaco, monospace;
|
|
423
|
+
font-weight: 500;
|
|
424
|
+
color: var(--text-primary);
|
|
425
|
+
}
|
|
426
|
+
.task-summary .stat-cell {
|
|
427
|
+
text-align: center;
|
|
428
|
+
min-width: 80px;
|
|
429
|
+
}
|
|
430
|
+
.task-summary .stat-cell .count {
|
|
431
|
+
font-weight: 600;
|
|
432
|
+
}
|
|
433
|
+
.task-summary .stat-cell .pct {
|
|
434
|
+
color: var(--text-dim);
|
|
435
|
+
font-size: 0.75rem;
|
|
436
|
+
margin-left: 0.25rem;
|
|
437
|
+
}
|
|
438
|
+
.task-summary .total-row {
|
|
439
|
+
font-weight: 600;
|
|
440
|
+
background: var(--bg-secondary);
|
|
441
|
+
}
|
|
442
|
+
.task-summary .total-row td {
|
|
443
|
+
border-bottom: none;
|
|
444
|
+
}
|
|
445
|
+
.task-summary .timing-cell {
|
|
446
|
+
white-space: nowrap;
|
|
447
|
+
}
|
|
448
|
+
.task-summary .timing-range {
|
|
449
|
+
color: var(--text-dim);
|
|
450
|
+
font-size: 0.7rem;
|
|
451
|
+
}
|
|
452
|
+
|
|
395
453
|
#family-table {
|
|
396
454
|
width: 100%;
|
|
397
455
|
border-collapse: collapse;
|
|
@@ -522,6 +580,9 @@
|
|
|
522
580
|
</div>
|
|
523
581
|
<div id="dag-flow"></div>
|
|
524
582
|
</div>
|
|
583
|
+
<div class="task-summary" id="task-summary" style="margin-top: 16px">
|
|
584
|
+
<!-- Populated by JS -->
|
|
585
|
+
</div>
|
|
525
586
|
<table id="family-table" style="margin-top: 16px">
|
|
526
587
|
<thead>
|
|
527
588
|
<tr>
|
|
@@ -755,10 +816,10 @@
|
|
|
755
816
|
);
|
|
756
817
|
|
|
757
818
|
// Result
|
|
758
|
-
if (job.
|
|
819
|
+
if (job.result !== null && job.result !== undefined) {
|
|
759
820
|
document.getElementById("result-card").style.display = "block";
|
|
760
821
|
document.getElementById("result-block").textContent = formatJson(
|
|
761
|
-
job.
|
|
822
|
+
job.result,
|
|
762
823
|
);
|
|
763
824
|
}
|
|
764
825
|
|
|
@@ -815,10 +876,21 @@
|
|
|
815
876
|
|
|
816
877
|
familyData = await res.json();
|
|
817
878
|
|
|
818
|
-
//
|
|
819
|
-
|
|
879
|
+
// Find the current job in the family
|
|
880
|
+
const currentJob = familyData.jobs.find((j) => j.id === jobId);
|
|
881
|
+
|
|
882
|
+
// Only show family view if:
|
|
883
|
+
// 1. Current job is the root (no parent_id) AND has children
|
|
884
|
+
// 2. Don't show for child jobs - they should just see their own info
|
|
885
|
+
const isRoot = currentJob && !currentJob.parent_id;
|
|
886
|
+
const hasChildren = familyData.jobs.some(
|
|
887
|
+
(j) => j.parent_id === jobId,
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
if (isRoot && familyData.jobs.length > 1) {
|
|
820
891
|
document.getElementById("family-card").style.display = "block";
|
|
821
892
|
renderDAG(familyData.jobs);
|
|
893
|
+
renderTaskSummary(familyData.jobs);
|
|
822
894
|
renderFamilyTable(familyData.jobs);
|
|
823
895
|
}
|
|
824
896
|
} catch (err) {
|
|
@@ -1190,6 +1262,149 @@
|
|
|
1190
1262
|
);
|
|
1191
1263
|
}
|
|
1192
1264
|
|
|
1265
|
+
function renderTaskSummary(jobs) {
|
|
1266
|
+
const container = document.getElementById("task-summary");
|
|
1267
|
+
const allStates = [
|
|
1268
|
+
"completed",
|
|
1269
|
+
"running",
|
|
1270
|
+
"available",
|
|
1271
|
+
"scheduled",
|
|
1272
|
+
"failed",
|
|
1273
|
+
"discarded",
|
|
1274
|
+
];
|
|
1275
|
+
const stateLabels = {
|
|
1276
|
+
completed: "Done",
|
|
1277
|
+
running: "Run",
|
|
1278
|
+
available: "Avail",
|
|
1279
|
+
scheduled: "Sched",
|
|
1280
|
+
failed: "Fail",
|
|
1281
|
+
discarded: "Disc",
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
function fmtDur(ms) {
|
|
1285
|
+
if (ms < 1000) return Math.round(ms) + "ms";
|
|
1286
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + "s";
|
|
1287
|
+
if (ms < 3600000) return (ms / 60000).toFixed(1) + "m";
|
|
1288
|
+
return (ms / 3600000).toFixed(1) + "h";
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const taskStats = {};
|
|
1292
|
+
let totals = { total: 0, durations: [] };
|
|
1293
|
+
allStates.forEach((s) => (totals[s] = 0));
|
|
1294
|
+
|
|
1295
|
+
jobs.forEach((job) => {
|
|
1296
|
+
const taskName = job.task_name;
|
|
1297
|
+
if (!taskStats[taskName]) {
|
|
1298
|
+
taskStats[taskName] = { total: 0, durations: [] };
|
|
1299
|
+
allStates.forEach((s) => (taskStats[taskName][s] = 0));
|
|
1300
|
+
}
|
|
1301
|
+
taskStats[taskName].total++;
|
|
1302
|
+
taskStats[taskName][job.state] =
|
|
1303
|
+
(taskStats[taskName][job.state] || 0) + 1;
|
|
1304
|
+
totals.total++;
|
|
1305
|
+
totals[job.state] = (totals[job.state] || 0) + 1;
|
|
1306
|
+
|
|
1307
|
+
// Include timing for completed, failed, and discarded jobs
|
|
1308
|
+
if (
|
|
1309
|
+
["completed", "failed", "discarded"].includes(job.state) &&
|
|
1310
|
+
job.attempted_at &&
|
|
1311
|
+
job.completed_at
|
|
1312
|
+
) {
|
|
1313
|
+
const duration =
|
|
1314
|
+
new Date(job.completed_at) - new Date(job.attempted_at);
|
|
1315
|
+
taskStats[taskName].durations.push(duration);
|
|
1316
|
+
totals.durations.push(duration);
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
// Show all state columns for consistency
|
|
1321
|
+
const states = allStates;
|
|
1322
|
+
|
|
1323
|
+
function calcDurationStats(durations) {
|
|
1324
|
+
if (durations.length === 0) return null;
|
|
1325
|
+
const sorted = durations.slice().sort((a, b) => a - b);
|
|
1326
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
1327
|
+
const p95Index = Math.floor(sorted.length * 0.95);
|
|
1328
|
+
return {
|
|
1329
|
+
avg: sum / sorted.length,
|
|
1330
|
+
p95: sorted[Math.min(p95Index, sorted.length - 1)],
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const taskNames = Object.keys(taskStats).sort();
|
|
1335
|
+
const headerCells = states
|
|
1336
|
+
.map((s) => `<th class="stat-cell">${stateLabels[s]}</th>`)
|
|
1337
|
+
.join("");
|
|
1338
|
+
|
|
1339
|
+
const rows = taskNames
|
|
1340
|
+
.map((taskName) => {
|
|
1341
|
+
const stats = taskStats[taskName];
|
|
1342
|
+
const shortName = taskName.split(".").pop();
|
|
1343
|
+
const ds = calcDurationStats(stats.durations);
|
|
1344
|
+
|
|
1345
|
+
const statCells = states
|
|
1346
|
+
.map((s) => {
|
|
1347
|
+
const count = stats[s] || 0;
|
|
1348
|
+
if (count === 0) return `<td class="stat-cell">-</td>`;
|
|
1349
|
+
const pct =
|
|
1350
|
+
stats.total > 0 ? Math.round((count / stats.total) * 100) : 0;
|
|
1351
|
+
return `<td class="stat-cell"><span class="count">${count}</span><span class="pct">(${pct}%)</span></td>`;
|
|
1352
|
+
})
|
|
1353
|
+
.join("");
|
|
1354
|
+
|
|
1355
|
+
const timing = ds
|
|
1356
|
+
? `${fmtDur(ds.avg)}<span class="timing-range"> (${fmtDur(ds.p95)})</span>`
|
|
1357
|
+
: "-";
|
|
1358
|
+
|
|
1359
|
+
return `<tr>
|
|
1360
|
+
<td class="task-name" title="${taskName}">${shortName}</td>
|
|
1361
|
+
<td class="stat-cell">${stats.total}</td>
|
|
1362
|
+
${statCells}
|
|
1363
|
+
<td class="stat-cell timing-cell">${timing}</td>
|
|
1364
|
+
</tr>`;
|
|
1365
|
+
})
|
|
1366
|
+
.join("");
|
|
1367
|
+
|
|
1368
|
+
const tds = calcDurationStats(totals.durations);
|
|
1369
|
+
const totalCells = states
|
|
1370
|
+
.map((s) => {
|
|
1371
|
+
const count = totals[s] || 0;
|
|
1372
|
+
if (count === 0) return `<td class="stat-cell">-</td>`;
|
|
1373
|
+
const pct =
|
|
1374
|
+
totals.total > 0 ? Math.round((count / totals.total) * 100) : 0;
|
|
1375
|
+
return `<td class="stat-cell"><span class="count">${count}</span><span class="pct">(${pct}%)</span></td>`;
|
|
1376
|
+
})
|
|
1377
|
+
.join("");
|
|
1378
|
+
|
|
1379
|
+
const totalTiming = tds
|
|
1380
|
+
? `${fmtDur(tds.avg)}<span class="timing-range"> (${fmtDur(tds.p95)})</span>`
|
|
1381
|
+
: "-";
|
|
1382
|
+
|
|
1383
|
+
const totalRow = `<tr class="total-row">
|
|
1384
|
+
<td>Total</td>
|
|
1385
|
+
<td class="stat-cell">${totals.total}</td>
|
|
1386
|
+
${totalCells}
|
|
1387
|
+
<td class="stat-cell timing-cell">${totalTiming}</td>
|
|
1388
|
+
</tr>`;
|
|
1389
|
+
|
|
1390
|
+
container.innerHTML = `
|
|
1391
|
+
<table>
|
|
1392
|
+
<thead>
|
|
1393
|
+
<tr>
|
|
1394
|
+
<th>Task</th>
|
|
1395
|
+
<th class="stat-cell">#</th>
|
|
1396
|
+
${headerCells}
|
|
1397
|
+
<th class="stat-cell">Avg (p95)</th>
|
|
1398
|
+
</tr>
|
|
1399
|
+
</thead>
|
|
1400
|
+
<tbody>
|
|
1401
|
+
${rows}
|
|
1402
|
+
${totalRow}
|
|
1403
|
+
</tbody>
|
|
1404
|
+
</table>
|
|
1405
|
+
`;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1193
1408
|
function renderFamilyTable(jobs) {
|
|
1194
1409
|
const tbody = document.querySelector("#family-table tbody");
|
|
1195
1410
|
|
|
@@ -1214,13 +1429,15 @@
|
|
|
1214
1429
|
}
|
|
1215
1430
|
|
|
1216
1431
|
let result = "-";
|
|
1217
|
-
if (job.
|
|
1218
|
-
const
|
|
1219
|
-
typeof job.
|
|
1220
|
-
? job.
|
|
1221
|
-
: JSON.stringify(job.
|
|
1432
|
+
if (job.result !== null && job.result !== undefined) {
|
|
1433
|
+
const resultStr =
|
|
1434
|
+
typeof job.result === "string"
|
|
1435
|
+
? job.result
|
|
1436
|
+
: JSON.stringify(job.result);
|
|
1222
1437
|
result =
|
|
1223
|
-
|
|
1438
|
+
resultStr.length > 30
|
|
1439
|
+
? resultStr.slice(0, 30) + "..."
|
|
1440
|
+
: resultStr;
|
|
1224
1441
|
}
|
|
1225
1442
|
|
|
1226
1443
|
return `
|
fairchild/ui.py
CHANGED
|
@@ -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
|
|
|
@@ -179,7 +159,7 @@ async def api_jobs(request: web.Request) -> web.Response:
|
|
|
179
159
|
parent_id, deps,
|
|
180
160
|
state, priority, scheduled_at,
|
|
181
161
|
attempted_at, completed_at, attempt, max_attempts,
|
|
182
|
-
|
|
162
|
+
result, errors, tags,
|
|
183
163
|
inserted_at, updated_at
|
|
184
164
|
FROM fairchild_jobs
|
|
185
165
|
{where}
|
|
@@ -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
|
|
|
@@ -312,7 +316,7 @@ async def api_job_detail(request: web.Request) -> web.Response:
|
|
|
312
316
|
parent_id, deps,
|
|
313
317
|
state, priority, scheduled_at,
|
|
314
318
|
attempted_at, completed_at, attempt, max_attempts,
|
|
315
|
-
|
|
319
|
+
result, errors, tags,
|
|
316
320
|
inserted_at, updated_at
|
|
317
321
|
FROM fairchild_jobs
|
|
318
322
|
WHERE id = $1
|
|
@@ -378,15 +382,15 @@ async def api_job_family(request: web.Request) -> web.Response:
|
|
|
378
382
|
# Get all descendants from the root
|
|
379
383
|
family_query = """
|
|
380
384
|
WITH RECURSIVE family AS (
|
|
381
|
-
SELECT id, task_name, parent_id, state, deps,
|
|
382
|
-
attempted_at, completed_at, attempt, max_attempts
|
|
385
|
+
SELECT id, task_name, parent_id, state, deps, result,
|
|
386
|
+
attempted_at, completed_at, updated_at, attempt, max_attempts
|
|
383
387
|
FROM fairchild_jobs
|
|
384
388
|
WHERE id = $1
|
|
385
389
|
|
|
386
390
|
UNION ALL
|
|
387
391
|
|
|
388
|
-
SELECT j.id, j.task_name, j.parent_id, j.state, j.deps, j.
|
|
389
|
-
j.attempted_at, j.completed_at, j.attempt, j.max_attempts
|
|
392
|
+
SELECT j.id, j.task_name, j.parent_id, j.state, j.deps, j.result,
|
|
393
|
+
j.attempted_at, j.completed_at, j.updated_at, j.attempt, j.max_attempts
|
|
390
394
|
FROM fairchild_jobs j
|
|
391
395
|
INNER JOIN family f ON j.parent_id = f.id
|
|
392
396
|
)
|
|
@@ -405,6 +409,7 @@ async def api_job_family(request: web.Request) -> web.Response:
|
|
|
405
409
|
job["completed_at"] = (
|
|
406
410
|
job["completed_at"].isoformat() if job["completed_at"] else None
|
|
407
411
|
)
|
|
412
|
+
job["updated_at"] = job["updated_at"].isoformat() if job["updated_at"] else None
|
|
408
413
|
jobs.append(job)
|
|
409
414
|
|
|
410
415
|
return web.json_response({"root_id": str(root_id), "jobs": jobs})
|
fairchild/worker.py
CHANGED
|
@@ -10,8 +10,7 @@ from uuid import uuid4
|
|
|
10
10
|
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
|
-
from fairchild.
|
|
14
|
-
from fairchild.task import get_task
|
|
13
|
+
from fairchild.task import get_task, get_task_schemas
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class Worker:
|
|
@@ -120,9 +119,9 @@ class Worker:
|
|
|
120
119
|
# Check if this is a future marker
|
|
121
120
|
if "__future__" in obj and len(obj) == 1:
|
|
122
121
|
job_id = obj["__future__"]
|
|
123
|
-
# Fetch the
|
|
122
|
+
# Fetch the result from the completed job
|
|
124
123
|
query = """
|
|
125
|
-
SELECT
|
|
124
|
+
SELECT result FROM fairchild_jobs
|
|
126
125
|
WHERE id = $1 AND state = 'completed'
|
|
127
126
|
"""
|
|
128
127
|
from uuid import UUID
|
|
@@ -180,19 +179,23 @@ class Worker:
|
|
|
180
179
|
for child_job in pending_children:
|
|
181
180
|
await self.fairchild._insert_job(child_job)
|
|
182
181
|
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
if
|
|
186
|
-
|
|
182
|
+
# Try to capture the return value as JSON
|
|
183
|
+
result_value = None
|
|
184
|
+
if result is not None:
|
|
185
|
+
try:
|
|
186
|
+
json.dumps(result) # Test if serializable
|
|
187
|
+
result_value = result
|
|
188
|
+
except (TypeError, ValueError):
|
|
189
|
+
pass # Not JSON-serializable, skip
|
|
187
190
|
|
|
188
191
|
# Check if this job spawned children - if so, wait for them
|
|
189
192
|
has_children = len(pending_children) > 0
|
|
190
193
|
if has_children:
|
|
191
194
|
# Keep job in running state, it will be completed when children finish
|
|
192
|
-
await self._mark_waiting_for_children(job,
|
|
195
|
+
await self._mark_waiting_for_children(job, result_value)
|
|
193
196
|
print(f"[{self.name}] Job {job.id} waiting for children to complete")
|
|
194
197
|
else:
|
|
195
|
-
await self._complete_job(job,
|
|
198
|
+
await self._complete_job(job, result_value)
|
|
196
199
|
print(f"[{self.name}] Completed job {job.id}")
|
|
197
200
|
|
|
198
201
|
except Exception as e:
|
|
@@ -214,19 +217,19 @@ class Worker:
|
|
|
214
217
|
f"[{self.name}] Job {job.id} failed (attempt {job.attempt}/{job.max_attempts}): {e}"
|
|
215
218
|
)
|
|
216
219
|
|
|
217
|
-
async def _complete_job(self, job: Job,
|
|
220
|
+
async def _complete_job(self, job: Job, result: Any | None):
|
|
218
221
|
"""Mark a job as completed."""
|
|
219
222
|
query = """
|
|
220
223
|
UPDATE fairchild_jobs
|
|
221
224
|
SET state = 'completed',
|
|
222
225
|
completed_at = now(),
|
|
223
|
-
|
|
226
|
+
result = $2,
|
|
224
227
|
updated_at = now()
|
|
225
228
|
WHERE id = $1
|
|
226
229
|
"""
|
|
227
230
|
|
|
228
|
-
|
|
229
|
-
await self.fairchild._pool.execute(query, job.id,
|
|
231
|
+
result_json = json.dumps(result) if result is not None else None
|
|
232
|
+
await self.fairchild._pool.execute(query, job.id, result_json)
|
|
230
233
|
|
|
231
234
|
# Check if this completion unblocks jobs waiting on this one
|
|
232
235
|
await self._check_child_deps(job)
|
|
@@ -246,17 +249,17 @@ class Worker:
|
|
|
246
249
|
"""
|
|
247
250
|
return await self.fairchild._pool.fetchval(query, job.id)
|
|
248
251
|
|
|
249
|
-
async def _mark_waiting_for_children(self, job: Job,
|
|
252
|
+
async def _mark_waiting_for_children(self, job: Job, result: Any | None):
|
|
250
253
|
"""Mark a job as waiting for children (stays in running state but stores result)."""
|
|
251
|
-
# Store the
|
|
254
|
+
# Store the result value so we can use it when completing later
|
|
252
255
|
query = """
|
|
253
256
|
UPDATE fairchild_jobs
|
|
254
|
-
SET
|
|
257
|
+
SET result = $2,
|
|
255
258
|
updated_at = now()
|
|
256
259
|
WHERE id = $1
|
|
257
260
|
"""
|
|
258
|
-
|
|
259
|
-
await self.fairchild._pool.execute(query, job.id,
|
|
261
|
+
result_json = json.dumps(result) if result is not None else None
|
|
262
|
+
await self.fairchild._pool.execute(query, job.id, result_json)
|
|
260
263
|
|
|
261
264
|
async def _check_parent_completion(self, parent_id):
|
|
262
265
|
"""Check if a parent job can be completed (all children done)."""
|
|
@@ -326,6 +329,7 @@ class Worker:
|
|
|
326
329
|
UPDATE fairchild_jobs
|
|
327
330
|
SET state = $2,
|
|
328
331
|
errors = errors || $3::jsonb,
|
|
332
|
+
completed_at = now(),
|
|
329
333
|
updated_at = now()
|
|
330
334
|
WHERE id = $1
|
|
331
335
|
"""
|
|
@@ -338,7 +342,7 @@ class Worker:
|
|
|
338
342
|
else:
|
|
339
343
|
query = """
|
|
340
344
|
UPDATE fairchild_jobs
|
|
341
|
-
SET state = $2, updated_at = now()
|
|
345
|
+
SET state = $2, completed_at = now(), updated_at = now()
|
|
342
346
|
WHERE id = $1
|
|
343
347
|
"""
|
|
344
348
|
await self.fairchild._pool.execute(query, job.id, new_state)
|
|
@@ -398,26 +402,30 @@ class WorkerPool:
|
|
|
398
402
|
async def _register(self):
|
|
399
403
|
"""Register this worker pool in the database."""
|
|
400
404
|
query = """
|
|
401
|
-
INSERT INTO fairchild_workers (id, hostname, pid, queues, state)
|
|
402
|
-
VALUES ($1, $2, $3, $4::jsonb, 'running')
|
|
405
|
+
INSERT INTO fairchild_workers (id, hostname, pid, queues, tasks, state)
|
|
406
|
+
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, 'running')
|
|
403
407
|
ON CONFLICT (id) DO UPDATE SET
|
|
404
408
|
hostname = EXCLUDED.hostname,
|
|
405
409
|
pid = EXCLUDED.pid,
|
|
406
410
|
queues = EXCLUDED.queues,
|
|
411
|
+
tasks = EXCLUDED.tasks,
|
|
407
412
|
state = 'running',
|
|
408
413
|
last_heartbeat_at = now()
|
|
409
414
|
"""
|
|
410
415
|
# asyncpg requires JSON as a string for jsonb columns
|
|
411
416
|
import json as json_module
|
|
412
417
|
|
|
418
|
+
tasks = get_task_schemas()
|
|
413
419
|
await self.fairchild._pool.execute(
|
|
414
420
|
query,
|
|
415
421
|
self.id,
|
|
416
422
|
self.hostname,
|
|
417
423
|
self.pid,
|
|
418
424
|
json_module.dumps(self.queue_config),
|
|
425
|
+
json_module.dumps(tasks),
|
|
419
426
|
)
|
|
420
427
|
print(f"Registered worker pool {self.id} ({self.hostname}:{self.pid})")
|
|
428
|
+
print(f" Tasks: {[t['name'] for t in tasks]}")
|
|
421
429
|
|
|
422
430
|
async def _heartbeat_loop(self):
|
|
423
431
|
"""Periodically send heartbeats and check for pause state."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
fairchild/__init__.py,sha256=80mYyoeUFY-85M5lipyZE-4QPTdJ2fqM6KsFUqAyytw,159
|
|
2
|
+
fairchild/cli.py,sha256=LOrF4e6LgMasWGmeRpciD6iDptC3IiiVF53Ll-TzN8M,9895
|
|
3
|
+
fairchild/context.py,sha256=64yEo5Cj4LeBT6LQd2UdN00tM7SXSd2Vw_dM4RV1u68,1757
|
|
4
|
+
fairchild/fairchild.py,sha256=QxGPvkhedyaEtjtdavIONgfX3O4H5BKo1hG1T2WFNJ4,4451
|
|
5
|
+
fairchild/future.py,sha256=SeVm_6Ds4k3qdwpKnFl71Qknx9tRM9v3UWZ_tJCp7cw,2302
|
|
6
|
+
fairchild/job.py,sha256=_ilXLHg1aQOM4xOUZArT-UDGzPQnKVDYKvru4Qz1MXs,3767
|
|
7
|
+
fairchild/task.py,sha256=TsOjs2UYt8qN8WV3YxOP_4ppzBARh4ySz4eM1O9z4II,8135
|
|
8
|
+
fairchild/ui.py,sha256=apOUmn7HZYg06F4J-Sf4EteV97U6YVmcFvE8fiHX7GI,18213
|
|
9
|
+
fairchild/worker.py,sha256=OIv_NnKHU3fZuIQvDZqvcJ7-2EOvmfIctksxAuEzu_8,17421
|
|
10
|
+
fairchild/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
fairchild/db/migrations.py,sha256=tL5UPqllCipYC-XcEH_4ZfaRZLtJKaPACp70ZusvEvg,2174
|
|
12
|
+
fairchild/db/migrations/001_initial.sql,sha256=FFObm3bIHdASd3d7lXzuAzWsmkCm3OAO2F63pw6HcMs,1711
|
|
13
|
+
fairchild/db/migrations/002_add_parent_id.sql,sha256=ftDT053BaMnXX42aDxTtHnRnAgNKzIaUd1Fz_sO6LyQ,295
|
|
14
|
+
fairchild/db/migrations/003_remove_workflows.sql,sha256=6svXO2nM7JiO-QHXfzIJCd6DeOSZ0AFQDsuQgPPv4IY,368
|
|
15
|
+
fairchild/db/migrations/004_add_workers.sql,sha256=SOvHykAinr_O9gDf7kllEUZ57pw-USoHUsYLCfQumMI,714
|
|
16
|
+
fairchild/db/migrations/005_add_worker_tasks.sql,sha256=0VJA5HBE2Aidjmk72nqLzreWA8A2jRDZsg1-6rcqTzo,186
|
|
17
|
+
fairchild/db/migrations/006_rename_recorded_to_result.sql,sha256=LAKa_VnK5hXIeUY9rJNWlOYGz5hBV7ttHuKHlevWCjY,97
|
|
18
|
+
fairchild/templates/dashboard.html,sha256=2kF3RLUMuZqQWbdlV-GPvYsBD8JnA_nWf4lBqRt60Sc,50071
|
|
19
|
+
fairchild/templates/job.html,sha256=ptliW9AGguv0EaouSH5HFWfz-JqpuTj17ih8LbR2eQw,45059
|
|
20
|
+
fairchild-0.0.6.dist-info/licenses/LICENSE,sha256=ad6qehkQLI1ax2pV6ocs7YePX06CPLBs3SThRbNC0q0,1068
|
|
21
|
+
fairchild-0.0.6.dist-info/METADATA,sha256=mBRHxvdGvnjGhRoFvAT_FnnfrAwbqqINfV5M4RwvIfk,11006
|
|
22
|
+
fairchild-0.0.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
23
|
+
fairchild-0.0.6.dist-info/entry_points.txt,sha256=urOgjfuYex5__jBX91srCX8T5GgHvYZDPpWYkuA7z90,49
|
|
24
|
+
fairchild-0.0.6.dist-info/top_level.txt,sha256=_2zkPnqS4i3JjLMpxRPDrBS0a04KBXZiD1NHFv7CO4U,10
|
|
25
|
+
fairchild-0.0.6.dist-info/RECORD,,
|
fairchild/record.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@dataclass(frozen=True)
|
|
6
|
-
class Record:
|
|
7
|
-
"""Wrapper to indicate a task's return value should be persisted.
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
@task(queue="default")
|
|
11
|
-
def my_task(item_id: int):
|
|
12
|
-
result = process(item_id)
|
|
13
|
-
return Record({"item_id": item_id, "result": result})
|
|
14
|
-
|
|
15
|
-
The recorded value will be stored in the job's `recorded` column
|
|
16
|
-
and can be retrieved by downstream jobs in a workflow.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
value: Any
|
|
20
|
-
|
|
21
|
-
def __repr__(self) -> str:
|
|
22
|
-
return f"Record({self.value!r})"
|
fairchild-0.0.4.dist-info/RECORD
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
fairchild/__init__.py,sha256=Q7FvQFBPDM12tf2hAg_nlROSdg7dYIXWnDwyqX-6mLU,209
|
|
2
|
-
fairchild/cli.py,sha256=H0Y1Zr6POsbnRCJIoMVh65jLYiPt9_-kPNP4_5MGrjQ,10077
|
|
3
|
-
fairchild/context.py,sha256=64yEo5Cj4LeBT6LQd2UdN00tM7SXSd2Vw_dM4RV1u68,1757
|
|
4
|
-
fairchild/fairchild.py,sha256=QxGPvkhedyaEtjtdavIONgfX3O4H5BKo1hG1T2WFNJ4,4451
|
|
5
|
-
fairchild/future.py,sha256=SeVm_6Ds4k3qdwpKnFl71Qknx9tRM9v3UWZ_tJCp7cw,2302
|
|
6
|
-
fairchild/job.py,sha256=_ilXLHg1aQOM4xOUZArT-UDGzPQnKVDYKvru4Qz1MXs,3767
|
|
7
|
-
fairchild/record.py,sha256=DjTQgeE2YjjsU2nB3pegB0Njq1_sNWgJialPLZQ1k7c,575
|
|
8
|
-
fairchild/task.py,sha256=Nwt8FxDV7PK4y4nMXingFPPmPX4HLhamlJrSacyXgIg,6964
|
|
9
|
-
fairchild/ui.py,sha256=q7Bj60PAulMtt5KM3UvkFqpGJuKvc4P8Vv3RN5tIirs,17806
|
|
10
|
-
fairchild/worker.py,sha256=cZzHLuE7CF9bTLy_59zUeZHQs_p7z2VUMJfcMZTjWLs,17045
|
|
11
|
-
fairchild/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
fairchild/db/migrations.py,sha256=tL5UPqllCipYC-XcEH_4ZfaRZLtJKaPACp70ZusvEvg,2174
|
|
13
|
-
fairchild/db/migrations/001_initial.sql,sha256=FFObm3bIHdASd3d7lXzuAzWsmkCm3OAO2F63pw6HcMs,1711
|
|
14
|
-
fairchild/db/migrations/002_add_parent_id.sql,sha256=ftDT053BaMnXX42aDxTtHnRnAgNKzIaUd1Fz_sO6LyQ,295
|
|
15
|
-
fairchild/db/migrations/003_remove_workflows.sql,sha256=6svXO2nM7JiO-QHXfzIJCd6DeOSZ0AFQDsuQgPPv4IY,368
|
|
16
|
-
fairchild/db/migrations/004_add_workers.sql,sha256=SOvHykAinr_O9gDf7kllEUZ57pw-USoHUsYLCfQumMI,714
|
|
17
|
-
fairchild/templates/dashboard.html,sha256=Ng0q9iXGdDSo74ltABUupzjbKhqpofC8S3Zw7wXUmLA,50041
|
|
18
|
-
fairchild/templates/job.html,sha256=Pg7hd5p8dU5XJeCW9SryvDbvcErY1AMrxYm0z8hldnA,38042
|
|
19
|
-
fairchild-0.0.4.dist-info/licenses/LICENSE,sha256=ad6qehkQLI1ax2pV6ocs7YePX06CPLBs3SThRbNC0q0,1068
|
|
20
|
-
fairchild-0.0.4.dist-info/METADATA,sha256=RKftba_0moODAJNOAQiwVeYtC6HmXJuKGerN4VyHZDE,11006
|
|
21
|
-
fairchild-0.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
22
|
-
fairchild-0.0.4.dist-info/entry_points.txt,sha256=urOgjfuYex5__jBX91srCX8T5GgHvYZDPpWYkuA7z90,49
|
|
23
|
-
fairchild-0.0.4.dist-info/top_level.txt,sha256=_2zkPnqS4i3JjLMpxRPDrBS0a04KBXZiD1NHFv7CO4U,10
|
|
24
|
-
fairchild-0.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|