fairchild 0.0.1__py3-none-any.whl → 0.0.2__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.
@@ -0,0 +1,483 @@
1
+ Metadata-Version: 2.4
2
+ Name: fairchild
3
+ Version: 0.0.2
4
+ Summary: Workflow scheduling with PostgreSQL
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: asyncpg>=0.29.0
9
+ Requires-Dist: click>=8.0.0
10
+ Requires-Dist: aiohttp>=3.9.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
13
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ # Fairchild
17
+
18
+ A PostgreSQL-backed job queue and simple workflow engine. Inspired by [Oban](https://oban.pro) and [Faktory](https://contribsys.com/faktory/), among others.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install fairchild
24
+ ```
25
+
26
+ Requires PostgreSQL 12+.
27
+
28
+ ## Quick Start
29
+
30
+ 1. Define a task:
31
+
32
+ ```python
33
+ # tasks.py
34
+ from fairchild import task, Record
35
+
36
+ @task(queue="default")
37
+ def send_email(to: str, subject: str, body: str):
38
+ # Your email sending logic here
39
+ print(f"Sending email to {to}: {subject}")
40
+ return Record({"sent": True})
41
+ ```
42
+
43
+ 2. Set up the database and enqueue a job:
44
+
45
+ ```python
46
+ import asyncio
47
+ from fairchild import Fairchild
48
+ import tasks # Import to register tasks
49
+
50
+ async def main():
51
+ fairchild = Fairchild("postgresql://localhost/myapp")
52
+ await fairchild.connect()
53
+
54
+ # Create the jobs table
55
+ await fairchild.install()
56
+
57
+ # Enqueue a job
58
+ tasks.send_email.enqueue(
59
+ to="user@example.com",
60
+ subject="Hello",
61
+ body="Welcome to Fairchild!"
62
+ )
63
+
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ 3. Run a worker:
68
+
69
+ ```bash
70
+ export FAIRCHILD_DATABASE_URL="postgresql://localhost/myapp"
71
+ fairchild worker --import tasks
72
+ ```
73
+
74
+ ## Defining Tasks
75
+
76
+ Use the `@task` decorator to define a task:
77
+
78
+ ```python
79
+ from fairchild import task
80
+
81
+ @task(
82
+ queue="default", # Queue name (default: "default")
83
+ max_attempts=3, # Retry attempts on failure (default: 3)
84
+ priority=5, # 0-9, lower = higher priority (default: 5)
85
+ tags=["email"], # Tags for filtering/categorization
86
+ )
87
+ def my_task(arg1: str, arg2: int):
88
+ # Task logic here
89
+ pass
90
+ ```
91
+
92
+ ### Returning Results
93
+
94
+ Use `Record()` to persist a task's result for use by downstream workflow jobs:
95
+
96
+ ```python
97
+ from fairchild import task, Record
98
+
99
+ @task()
100
+ def fetch_data(url: str):
101
+ data = requests.get(url).json()
102
+ return Record(data) # Stored in the database
103
+ ```
104
+
105
+ ## Enqueuing Jobs
106
+
107
+ ### Basic Enqueue
108
+
109
+ ```python
110
+ my_task.enqueue(arg1="hello", arg2=42)
111
+ ```
112
+
113
+ ### Schedule for Later
114
+
115
+ ```python
116
+ # Run in 30 minutes
117
+ my_task.enqueue_in(minutes=30, arg1="hello", arg2=42)
118
+
119
+ # Run at a specific time
120
+ from datetime import datetime, timezone
121
+ my_task.enqueue_at(
122
+ datetime(2024, 1, 15, 10, 0, tzinfo=timezone.utc),
123
+ arg1="hello",
124
+ arg2=42
125
+ )
126
+ ```
127
+
128
+ ### With Options
129
+
130
+ ```python
131
+ my_task.enqueue(
132
+ arg1="hello",
133
+ arg2=42,
134
+ _priority=1, # Override default priority
135
+ _queue="high", # Override default queue
136
+ )
137
+ ```
138
+
139
+ ## Dynamic Workflows
140
+
141
+ Fairchild uses a futures-based approach to workflows. Instead of explicitly declaring a DAG, you write natural Python code—call tasks, get futures, pass them around. Dependencies are inferred automatically.
142
+
143
+ ### How It Works
144
+
145
+ When a task calls another task from inside a worker:
146
+ 1. A child job is spawned (not executed immediately)
147
+ 2. A `Future` is returned representing the pending result
148
+ 3. If you pass that `Future` to another task, a dependency is created
149
+ 4. The downstream task won't run until the upstream job completes
150
+
151
+ ### Basic Example
152
+
153
+ ```python
154
+ from fairchild import task, Record
155
+
156
+ @task()
157
+ def fetch_data(url: str):
158
+ data = requests.get(url).json()
159
+ return Record(data)
160
+
161
+ @task()
162
+ def process(data: dict):
163
+ # Process the data
164
+ return Record({"processed": True})
165
+
166
+ @task()
167
+ def orchestrator():
168
+ # Calling a task returns a Future
169
+ data = fetch_data(url="https://api.example.com/data")
170
+
171
+ # Passing the Future creates a dependency
172
+ # process() won't run until fetch_data() completes
173
+ result = process(data=data)
174
+
175
+ return Record({"started": True})
176
+ ```
177
+
178
+ ### Fan-Out / Fan-In (Map-Reduce)
179
+
180
+ The futures model makes parallel processing with aggregation natural:
181
+
182
+ ```python
183
+ @task()
184
+ def multiply(x: int, y: int):
185
+ return Record(x * y)
186
+
187
+ @task()
188
+ def sum_results(values: list):
189
+ return Record(sum(values))
190
+
191
+ @task()
192
+ def orchestrator(items: list[int]):
193
+ # Fan-out: spawn parallel tasks, collect futures
194
+ futures = []
195
+ for item in items:
196
+ future = multiply(x=item, y=2)
197
+ futures.append(future)
198
+
199
+ # Fan-in: pass all futures to aggregator
200
+ # sum_results won't run until ALL multiply jobs complete
201
+ total = sum_results(values=futures)
202
+
203
+ return Record({"spawned": len(items) + 1})
204
+ ```
205
+
206
+ When `orchestrator([1, 2, 3])` runs, it creates this DAG:
207
+
208
+ ```
209
+ orchestrator
210
+ ├── multiply(1, 2) ──┐
211
+ ├── multiply(2, 2) ──┼── sum_results([...])
212
+ └── multiply(3, 2) ──┘
213
+ ```
214
+
215
+ ### Nested Workflows
216
+
217
+ Since it's just function calls, workflows can be arbitrarily nested:
218
+
219
+ ```python
220
+ @task()
221
+ def process_batch(batch_id: int):
222
+ # This task can spawn its own sub-workflow
223
+ futures = [process_item(item_id=i) for i in get_items(batch_id)]
224
+ return aggregate(results=futures)
225
+
226
+ @task()
227
+ def run_all_batches():
228
+ # Top-level orchestrator spawns batch processors
229
+ futures = [process_batch(batch_id=i) for i in range(10)]
230
+ return final_summary(batch_results=futures)
231
+ ```
232
+
233
+ ### Accessing Results
234
+
235
+ When a Future is passed to a downstream task, Fairchild automatically resolves it to the actual value before the task runs:
236
+
237
+ ```python
238
+ @task()
239
+ def fetch_price(symbol: str):
240
+ price = get_stock_price(symbol)
241
+ return Record({"symbol": symbol, "price": price})
242
+
243
+ @task()
244
+ def calculate_total(prices: list):
245
+ # By the time this runs, prices is a list of actual values,
246
+ # not Futures - Fairchild resolves them automatically
247
+ total = sum(p["price"] for p in prices)
248
+ return Record({"total": total})
249
+
250
+ @task()
251
+ def portfolio_value(symbols: list[str]):
252
+ futures = [fetch_price(symbol=s) for s in symbols]
253
+ return calculate_total(prices=futures)
254
+ ```
255
+
256
+ ## Workers
257
+
258
+ ### CLI
259
+
260
+ ```bash
261
+ # Basic worker
262
+ fairchild worker --import myapp.tasks
263
+
264
+ # Multiple queues with concurrency
265
+ fairchild worker --import myapp.tasks --queues default,high,low --concurrency 10
266
+
267
+ # Specific queues only
268
+ fairchild worker --import myapp.tasks --queues critical
269
+ ```
270
+
271
+ ### Programmatic
272
+
273
+ ```python
274
+ from fairchild import Fairchild
275
+ from fairchild.worker import WorkerPool
276
+
277
+ async def main():
278
+ fairchild = Fairchild("postgresql://localhost/myapp")
279
+ await fairchild.connect()
280
+
281
+ pool = WorkerPool(
282
+ fairchild,
283
+ queues=["default", "high"],
284
+ concurrency=5,
285
+ )
286
+
287
+ await pool.start()
288
+
289
+ asyncio.run(main())
290
+ ```
291
+
292
+ ## Web UI
293
+
294
+ Fairchild includes a web dashboard for monitoring jobs and workflows.
295
+
296
+ ```bash
297
+ fairchild ui --import myapp.tasks --port 8080
298
+ ```
299
+
300
+ Then open http://localhost:8080
301
+
302
+ The UI provides:
303
+
304
+ - **Dashboard**: Job stats, queues, recent jobs, jobs-per-minute chart
305
+ - **Workflow view**: DAG visualization, job states, timing
306
+ - **Job details**: Arguments, results, errors, timeline
307
+
308
+ ### Theming
309
+
310
+ The UI supports light and dark modes. It respects your system preference and includes a manual toggle.
311
+
312
+ ## HTTP API
313
+
314
+ The web UI also exposes a JSON API.
315
+
316
+ ### Enqueue a Job
317
+
318
+ ```bash
319
+ POST /api/jobs
320
+ Content-Type: application/json
321
+
322
+ {
323
+ "task": "myapp.tasks.send_email",
324
+ "args": {
325
+ "to": "user@example.com",
326
+ "subject": "Hello"
327
+ },
328
+ "priority": 1,
329
+ "scheduled_at": "2024-01-15T10:00:00Z"
330
+ }
331
+ ```
332
+
333
+ Response:
334
+ ```json
335
+ {
336
+ "id": "550e8400-e29b-41d4-a716-446655440000",
337
+ "task": "myapp.tasks.send_email",
338
+ "queue": "default",
339
+ "state": "available",
340
+ "scheduled_at": "2024-01-15T10:00:00+00:00"
341
+ }
342
+ ```
343
+
344
+ ### Other Endpoints
345
+
346
+ - `GET /api/stats` - Job counts by state
347
+ - `GET /api/jobs` - List jobs (supports `?state=` and `?queue=` filters)
348
+ - `GET /api/jobs/{id}` - Job details
349
+ - `GET /api/queues` - Queue statistics
350
+ - `GET /api/workflows` - List workflows
351
+ - `GET /api/workflows/{id}` - Workflow details with all jobs
352
+
353
+ ## CLI Reference
354
+
355
+ ### `fairchild install`
356
+
357
+ Create the `fairchild_jobs` table:
358
+
359
+ ```bash
360
+ fairchild install
361
+ ```
362
+
363
+ ### `fairchild migrate`
364
+
365
+ Run pending migrations:
366
+
367
+ ```bash
368
+ fairchild migrate
369
+ ```
370
+
371
+ ### `fairchild worker`
372
+
373
+ Start a worker process:
374
+
375
+ ```bash
376
+ fairchild worker [OPTIONS]
377
+
378
+ Options:
379
+ --import TEXT Python module(s) to import (registers tasks)
380
+ --queues TEXT Comma-separated queue names (default: all)
381
+ --concurrency INT Number of concurrent jobs (default: 10)
382
+ ```
383
+
384
+ ### `fairchild ui`
385
+
386
+ Start the web UI:
387
+
388
+ ```bash
389
+ fairchild ui [OPTIONS]
390
+
391
+ Options:
392
+ --import TEXT Python module(s) to import (registers tasks)
393
+ --host TEXT Host to bind (default: localhost)
394
+ --port INT Port to bind (default: 8080)
395
+ ```
396
+
397
+ ### `fairchild enqueue`
398
+
399
+ Enqueue a job from the command line:
400
+
401
+ ```bash
402
+ fairchild enqueue myapp.tasks.my_task --args '{"key": "value"}'
403
+ ```
404
+
405
+ ### `fairchild run`
406
+
407
+ Run a task locally for testing (does not enqueue):
408
+
409
+ ```bash
410
+ fairchild run [OPTIONS] TASK_NAME
411
+
412
+ Options:
413
+ -i, --import TEXT Python module(s) to import (registers tasks)
414
+ -a, --arg TEXT Task argument as key=value
415
+ ```
416
+
417
+ Examples:
418
+ ```bash
419
+ # Simple invocation
420
+ fairchild run -i myapp.tasks myapp.tasks.hello -a name=World
421
+
422
+ # Multiple arguments
423
+ fairchild run -i myapp.tasks myapp.tasks.add -a a=2 -a b=3
424
+ ```
425
+
426
+ This runs the task function directly in the current process without involving the database or workers. Useful for testing and debugging—full tracebacks are printed on errors.
427
+
428
+ ## Testing
429
+
430
+ ### Running Tests Locally
431
+
432
+ 1. Create a test database:
433
+
434
+ ```bash
435
+ createdb fairchild_test
436
+ ```
437
+
438
+ 2. Run the tests with your development database URL - the tests will automatically use `_test` instead of `_development`:
439
+
440
+ ```bash
441
+ FAIRCHILD_DATABASE_URL=postgres://postgres@localhost/fairchild_development uv run pytest
442
+ ```
443
+
444
+ Or run specific test files:
445
+
446
+ ```bash
447
+ # Unit tests only (no database required)
448
+ uv run pytest tests/test_task.py tests/test_job.py tests/test_record.py
449
+
450
+ # Integration tests (requires database)
451
+ FAIRCHILD_DATABASE_URL=postgres://postgres@localhost/fairchild_development uv run pytest tests/test_integration.py
452
+
453
+ # Web UI tests (requires database)
454
+ FAIRCHILD_DATABASE_URL=postgres://postgres@localhost/fairchild_development uv run pytest tests/test_web_ui.py
455
+ ```
456
+
457
+ **Note:** Integration tests automatically convert `_development` to `_test` in the database URL to protect your development data.
458
+
459
+ ## Configuration
460
+
461
+ ### Environment Variables
462
+
463
+ | Variable | Description | Default |
464
+ |----------|-------------|---------|
465
+ | `FAIRCHILD_DATABASE_URL` | PostgreSQL connection URL | (required) |
466
+
467
+ ### Database URL Format
468
+
469
+ ```
470
+ postgresql://user:password@host:port/database
471
+ ```
472
+
473
+ Examples:
474
+ ```bash
475
+ # Local development
476
+ export FAIRCHILD_DATABASE_URL="postgresql://localhost/myapp_development"
477
+
478
+ # With credentials
479
+ export FAIRCHILD_DATABASE_URL="postgresql://myuser:mypass@localhost/myapp"
480
+
481
+ # Remote server
482
+ export FAIRCHILD_DATABASE_URL="postgresql://user:pass@db.example.com:5432/myapp"
483
+ ```
@@ -0,0 +1,18 @@
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-0.0.2.dist-info/licenses/LICENSE,sha256=ad6qehkQLI1ax2pV6ocs7YePX06CPLBs3SThRbNC0q0,1068
14
+ fairchild-0.0.2.dist-info/METADATA,sha256=RmIq3B7D1-Tr5_cynagjF_VtrR2NeSIq-3pok5FPn34,11006
15
+ fairchild-0.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
16
+ fairchild-0.0.2.dist-info/entry_points.txt,sha256=urOgjfuYex5__jBX91srCX8T5GgHvYZDPpWYkuA7z90,49
17
+ fairchild-0.0.2.dist-info/top_level.txt,sha256=_2zkPnqS4i3JjLMpxRPDrBS0a04KBXZiD1NHFv7CO4U,10
18
+ fairchild-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fairchild = fairchild.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Marsh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ fairchild
@@ -1,6 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: fairchild
3
- Version: 0.0.1
4
- Summary: Workflow scheduling
5
- Requires-Python: >=3.13
6
- Description-Content-Type: text/markdown
@@ -1,5 +0,0 @@
1
- main.py,sha256=Zujt49CbW6vsCcYSa4Y92OKgO7Hpk08GTVOasi8uJYM,87
2
- fairchild-0.0.1.dist-info/METADATA,sha256=mxi3x0KqGERlZ7uENP8xj5gIgtdw-xhBLKK0ECrqQis,146
3
- fairchild-0.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
4
- fairchild-0.0.1.dist-info/top_level.txt,sha256=ZAMgPdWghn6xTRBO6Kc3ML1y3ZrZLnjZlqbboKXc_AE,5
5
- fairchild-0.0.1.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- main
main.py DELETED
@@ -1,6 +0,0 @@
1
- def main():
2
- print("Hello from fairchild!")
3
-
4
-
5
- if __name__ == "__main__":
6
- main()