fairchild 0.0.1__tar.gz → 0.0.3__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.3/LICENSE +21 -0
- fairchild-0.0.3/PKG-INFO +483 -0
- fairchild-0.0.3/README.md +468 -0
- fairchild-0.0.3/fairchild/__init__.py +11 -0
- fairchild-0.0.3/fairchild/cli.py +386 -0
- fairchild-0.0.3/fairchild/context.py +54 -0
- fairchild-0.0.3/fairchild/db/migrations.py +69 -0
- fairchild-0.0.3/fairchild/fairchild.py +166 -0
- fairchild-0.0.3/fairchild/future.py +78 -0
- fairchild-0.0.3/fairchild/job.py +123 -0
- fairchild-0.0.3/fairchild/record.py +22 -0
- fairchild-0.0.3/fairchild/task.py +225 -0
- fairchild-0.0.3/fairchild/templates/dashboard.html +1650 -0
- fairchild-0.0.3/fairchild/templates/job.html +1245 -0
- fairchild-0.0.3/fairchild/ui.py +560 -0
- fairchild-0.0.3/fairchild/worker.py +495 -0
- fairchild-0.0.3/fairchild.egg-info/PKG-INFO +483 -0
- fairchild-0.0.3/fairchild.egg-info/SOURCES.txt +29 -0
- fairchild-0.0.3/fairchild.egg-info/entry_points.txt +2 -0
- fairchild-0.0.3/fairchild.egg-info/requires.txt +7 -0
- fairchild-0.0.3/fairchild.egg-info/top_level.txt +1 -0
- fairchild-0.0.3/pyproject.toml +23 -0
- fairchild-0.0.3/tests/test_cli.py +101 -0
- fairchild-0.0.3/tests/test_integration.py +149 -0
- fairchild-0.0.3/tests/test_job.py +76 -0
- fairchild-0.0.3/tests/test_record.py +40 -0
- fairchild-0.0.3/tests/test_task.py +62 -0
- fairchild-0.0.3/tests/test_web_ui.py +211 -0
- fairchild-0.0.1/PKG-INFO +0 -6
- fairchild-0.0.1/fairchild.egg-info/PKG-INFO +0 -6
- fairchild-0.0.1/fairchild.egg-info/SOURCES.txt +0 -7
- fairchild-0.0.1/fairchild.egg-info/top_level.txt +0 -1
- fairchild-0.0.1/main.py +0 -6
- fairchild-0.0.1/pyproject.toml +0 -7
- /fairchild-0.0.1/README.md → /fairchild-0.0.3/fairchild/db/__init__.py +0 -0
- {fairchild-0.0.1 → fairchild-0.0.3}/fairchild.egg-info/dependency_links.txt +0 -0
- {fairchild-0.0.1 → fairchild-0.0.3}/setup.cfg +0 -0
fairchild-0.0.3/LICENSE
ADDED
|
@@ -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.
|
fairchild-0.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fairchild
|
|
3
|
+
Version: 0.0.3
|
|
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
|
+
```
|