aegis-stack 0.1.0__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.
Potentially problematic release.
This version of aegis-stack might be problematic. Click here for more details.
- aegis/__init__.py +5 -0
- aegis/__main__.py +374 -0
- aegis/core/CLAUDE.md +365 -0
- aegis/core/__init__.py +6 -0
- aegis/core/components.py +115 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/template_generator.py +163 -0
- aegis/templates/CLAUDE.md +306 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis_stack-0.1.0.dist-info/METADATA +114 -0
- aegis_stack-0.1.0.dist-info/RECORD +103 -0
- aegis_stack-0.1.0.dist-info/WHEEL +4 -0
- aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Scheduler Component
|
|
2
|
+
|
|
3
|
+
The Scheduler Component provides background task processing and cron job capabilities using [APScheduler](https://apscheduler.readthedocs.io/).
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
{{ cookiecutter.project_name }} includes a scheduler component that runs as an independent service, enabling:
|
|
8
|
+
|
|
9
|
+
- **Background job processing**
|
|
10
|
+
- **Cron-style scheduled tasks**
|
|
11
|
+
- **Async job execution**
|
|
12
|
+
- **Job persistence and recovery**
|
|
13
|
+
- **Real-time job monitoring**
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
graph TB
|
|
19
|
+
subgraph "{{ cookiecutter.project_name }} Architecture"
|
|
20
|
+
API[FastAPI Backend<br/>Port 8000]
|
|
21
|
+
Scheduler[Scheduler Service<br/>Background Jobs]
|
|
22
|
+
Jobs[(Job Queue<br/>In-Memory)]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
subgraph "Job Types"
|
|
26
|
+
SystemJobs[System Maintenance<br/>Every 6 hours]
|
|
27
|
+
CustomJobs[Custom Jobs<br/>User-defined]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
API -.-> Scheduler
|
|
31
|
+
Scheduler --> Jobs
|
|
32
|
+
Jobs --> SystemJobs
|
|
33
|
+
Jobs --> CustomJobs
|
|
34
|
+
|
|
35
|
+
style Scheduler fill:#f9f,stroke:#333,stroke-width:2px
|
|
36
|
+
style Jobs fill:#bbf,stroke:#333,stroke-width:2px
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Running the Scheduler
|
|
40
|
+
|
|
41
|
+
The scheduler runs as a Docker service. Use these commands:
|
|
42
|
+
|
|
43
|
+
### Docker Deployment
|
|
44
|
+
```bash
|
|
45
|
+
# Run only scheduler service
|
|
46
|
+
docker compose --profile dev up scheduler
|
|
47
|
+
|
|
48
|
+
# Run all services including scheduler
|
|
49
|
+
make run
|
|
50
|
+
|
|
51
|
+
# Or use docker compose directly
|
|
52
|
+
docker compose --profile dev up
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Job Configuration
|
|
56
|
+
|
|
57
|
+
The scheduler is configured in `app/components/scheduler/main.py`:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
61
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
62
|
+
|
|
63
|
+
def create_scheduler() -> AsyncIOScheduler:
|
|
64
|
+
scheduler = AsyncIOScheduler()
|
|
65
|
+
|
|
66
|
+
# Add your jobs here
|
|
67
|
+
scheduler.add_job(
|
|
68
|
+
func=system_maintenance_job,
|
|
69
|
+
trigger=CronTrigger(hour=2), # Run daily at 2 AM
|
|
70
|
+
id="system_maintenance",
|
|
71
|
+
name="System Maintenance",
|
|
72
|
+
replace_existing=True
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return scheduler
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Adding Custom Jobs
|
|
79
|
+
|
|
80
|
+
### 1. Create Job Function
|
|
81
|
+
Create job functions in `app/services/`:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# app/services/my_jobs.py
|
|
85
|
+
import asyncio
|
|
86
|
+
from app.core.log import logger
|
|
87
|
+
|
|
88
|
+
async def process_daily_reports():
|
|
89
|
+
"""Generate daily reports."""
|
|
90
|
+
logger.info("Starting daily report generation")
|
|
91
|
+
|
|
92
|
+
# Your job logic here
|
|
93
|
+
await asyncio.sleep(1) # Simulate work
|
|
94
|
+
|
|
95
|
+
logger.info("Daily reports completed")
|
|
96
|
+
|
|
97
|
+
async def cleanup_old_files():
|
|
98
|
+
"""Clean up old temporary files."""
|
|
99
|
+
logger.info("Starting file cleanup")
|
|
100
|
+
|
|
101
|
+
# Your cleanup logic here
|
|
102
|
+
|
|
103
|
+
logger.info("File cleanup completed")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 2. Register Jobs in Scheduler
|
|
107
|
+
Add jobs to the scheduler configuration:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# app/components/scheduler/main.py
|
|
111
|
+
from app.services.my_jobs import process_daily_reports, cleanup_old_files
|
|
112
|
+
|
|
113
|
+
def create_scheduler() -> AsyncIOScheduler:
|
|
114
|
+
scheduler = AsyncIOScheduler()
|
|
115
|
+
|
|
116
|
+
# Daily report at 6 AM
|
|
117
|
+
scheduler.add_job(
|
|
118
|
+
func=process_daily_reports,
|
|
119
|
+
trigger=CronTrigger(hour=6, minute=0),
|
|
120
|
+
id="daily_reports",
|
|
121
|
+
name="Daily Report Generation"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Weekly cleanup on Sundays at midnight
|
|
125
|
+
scheduler.add_job(
|
|
126
|
+
func=cleanup_old_files,
|
|
127
|
+
trigger=CronTrigger(day_of_week="sun", hour=0, minute=0),
|
|
128
|
+
id="weekly_cleanup",
|
|
129
|
+
name="Weekly File Cleanup"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return scheduler
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Job Scheduling Options
|
|
136
|
+
|
|
137
|
+
### Cron-Style Triggers
|
|
138
|
+
```python
|
|
139
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
140
|
+
|
|
141
|
+
# Every day at 2:30 AM
|
|
142
|
+
CronTrigger(hour=2, minute=30)
|
|
143
|
+
|
|
144
|
+
# Every Monday at 9:00 AM
|
|
145
|
+
CronTrigger(day_of_week="mon", hour=9, minute=0)
|
|
146
|
+
|
|
147
|
+
# Every 15 minutes
|
|
148
|
+
CronTrigger(minute="*/15")
|
|
149
|
+
|
|
150
|
+
# First day of every month at midnight
|
|
151
|
+
CronTrigger(day=1, hour=0, minute=0)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Interval Triggers
|
|
155
|
+
```python
|
|
156
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
157
|
+
|
|
158
|
+
# Every 5 minutes
|
|
159
|
+
IntervalTrigger(minutes=5)
|
|
160
|
+
|
|
161
|
+
# Every 2 hours
|
|
162
|
+
IntervalTrigger(hours=2)
|
|
163
|
+
|
|
164
|
+
# Every 30 seconds
|
|
165
|
+
IntervalTrigger(seconds=30)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### One-Time Jobs
|
|
169
|
+
```python
|
|
170
|
+
from datetime import datetime, timedelta
|
|
171
|
+
|
|
172
|
+
# Run once in 1 hour
|
|
173
|
+
scheduler.add_job(
|
|
174
|
+
func=one_time_task,
|
|
175
|
+
trigger="date",
|
|
176
|
+
run_date=datetime.now() + timedelta(hours=1),
|
|
177
|
+
id="one_time_task"
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Job Management
|
|
182
|
+
|
|
183
|
+
### Listing Jobs
|
|
184
|
+
```python
|
|
185
|
+
# Get all scheduled jobs
|
|
186
|
+
jobs = scheduler.get_jobs()
|
|
187
|
+
for job in jobs:
|
|
188
|
+
print(f"Job: {job.name}, Next run: {job.next_run_time}")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Modifying Jobs
|
|
192
|
+
```python
|
|
193
|
+
# Pause a job
|
|
194
|
+
scheduler.pause_job("daily_reports")
|
|
195
|
+
|
|
196
|
+
# Resume a job
|
|
197
|
+
scheduler.resume_job("daily_reports")
|
|
198
|
+
|
|
199
|
+
# Remove a job
|
|
200
|
+
scheduler.remove_job("old_job_id")
|
|
201
|
+
|
|
202
|
+
# Modify job schedule
|
|
203
|
+
scheduler.modify_job("daily_reports", hour=7) # Change to 7 AM
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Error Handling
|
|
207
|
+
|
|
208
|
+
### Job Error Handling
|
|
209
|
+
```python
|
|
210
|
+
async def robust_job():
|
|
211
|
+
"""A job with proper error handling."""
|
|
212
|
+
try:
|
|
213
|
+
logger.info("Starting robust job")
|
|
214
|
+
|
|
215
|
+
# Job logic here
|
|
216
|
+
result = await some_async_operation()
|
|
217
|
+
|
|
218
|
+
logger.info(f"Job completed successfully: {result}")
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f"Job failed: {str(e)}", exc_info=True)
|
|
222
|
+
# Optionally send alerts or retry logic
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Scheduler Error Listeners
|
|
226
|
+
```python
|
|
227
|
+
def job_listener(event):
|
|
228
|
+
"""Listen for job events."""
|
|
229
|
+
if event.exception:
|
|
230
|
+
logger.error(f"Job {event.job_id} crashed: {event.exception}")
|
|
231
|
+
else:
|
|
232
|
+
logger.info(f"Job {event.job_id} executed successfully")
|
|
233
|
+
|
|
234
|
+
# Add listener to scheduler
|
|
235
|
+
scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Monitoring Jobs
|
|
239
|
+
|
|
240
|
+
### Health Checks
|
|
241
|
+
The scheduler health is included in system health checks:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
{{ cookiecutter.project_slug }} health check --detailed
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Shows scheduler status, active jobs, and next execution times.
|
|
248
|
+
|
|
249
|
+
### Job Logging
|
|
250
|
+
All job executions are logged with structured logging:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"timestamp": "2024-01-01T02:00:00Z",
|
|
255
|
+
"level": "INFO",
|
|
256
|
+
"logger": "scheduler",
|
|
257
|
+
"message": "Job executed successfully",
|
|
258
|
+
"job_id": "system_maintenance",
|
|
259
|
+
"execution_time_ms": 1250
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Custom Job Metrics
|
|
264
|
+
Add metrics to your jobs:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
import time
|
|
268
|
+
from app.core.log import logger
|
|
269
|
+
|
|
270
|
+
async def monitored_job():
|
|
271
|
+
start_time = time.time()
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
# Job work here
|
|
275
|
+
await do_work()
|
|
276
|
+
|
|
277
|
+
execution_time = time.time() - start_time
|
|
278
|
+
logger.info(f"Job completed in {execution_time:.2f}s")
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
execution_time = time.time() - start_time
|
|
282
|
+
logger.error(f"Job failed after {execution_time:.2f}s: {e}")
|
|
283
|
+
raise
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Configuration
|
|
287
|
+
|
|
288
|
+
### Scheduler Settings
|
|
289
|
+
Configure scheduler behavior in `app/core/config.py`:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
class Settings(BaseSettings):
|
|
293
|
+
# Scheduler configuration
|
|
294
|
+
scheduler_timezone: str = "UTC"
|
|
295
|
+
scheduler_max_workers: int = 10
|
|
296
|
+
scheduler_job_defaults: dict = {
|
|
297
|
+
"coalesce": False,
|
|
298
|
+
"max_instances": 3
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Job Persistence
|
|
303
|
+
For production deployments, consider using persistent job storage:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
307
|
+
|
|
308
|
+
jobstores = {
|
|
309
|
+
'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
scheduler = AsyncIOScheduler(jobstores=jobstores)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Best Practices
|
|
316
|
+
|
|
317
|
+
### 1. Keep Jobs Small and Focused
|
|
318
|
+
```python
|
|
319
|
+
# Good: Small, focused job
|
|
320
|
+
async def send_daily_summary():
|
|
321
|
+
await generate_summary()
|
|
322
|
+
await send_email()
|
|
323
|
+
|
|
324
|
+
# Better: Break into smaller jobs
|
|
325
|
+
async def generate_daily_summary():
|
|
326
|
+
return await generate_summary()
|
|
327
|
+
|
|
328
|
+
async def send_summary_email():
|
|
329
|
+
summary = await get_cached_summary()
|
|
330
|
+
await send_email(summary)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 2. Use Proper Async Patterns
|
|
334
|
+
```python
|
|
335
|
+
# Good: Use async/await consistently
|
|
336
|
+
async def async_job():
|
|
337
|
+
async with httpx.AsyncClient() as client:
|
|
338
|
+
response = await client.get("https://api.example.com")
|
|
339
|
+
return response.json()
|
|
340
|
+
|
|
341
|
+
# Avoid: Blocking operations
|
|
342
|
+
def bad_job():
|
|
343
|
+
response = requests.get("https://api.example.com") # Blocks event loop
|
|
344
|
+
return response.json()
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### 3. Handle Job Dependencies
|
|
348
|
+
```python
|
|
349
|
+
async def dependent_job():
|
|
350
|
+
# Check if prerequisite job completed
|
|
351
|
+
if not await check_prerequisite_completed():
|
|
352
|
+
logger.warning("Prerequisite not completed, skipping job")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
await perform_dependent_work()
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### 4. Use Idempotent Jobs
|
|
359
|
+
```python
|
|
360
|
+
async def idempotent_job():
|
|
361
|
+
# Check if work already done today
|
|
362
|
+
if await is_work_already_done_today():
|
|
363
|
+
logger.info("Work already completed today")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
await perform_work()
|
|
367
|
+
await mark_work_completed()
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Troubleshooting
|
|
371
|
+
|
|
372
|
+
### Common Issues
|
|
373
|
+
|
|
374
|
+
**Jobs not executing:**
|
|
375
|
+
- Check if scheduler service is running
|
|
376
|
+
- Verify job is properly registered
|
|
377
|
+
- Check timezone settings
|
|
378
|
+
- Review job logs for errors
|
|
379
|
+
|
|
380
|
+
**High memory usage:**
|
|
381
|
+
- Monitor job execution times
|
|
382
|
+
- Check for memory leaks in job functions
|
|
383
|
+
- Consider job queue limits
|
|
384
|
+
|
|
385
|
+
**Jobs running too frequently:**
|
|
386
|
+
- Review cron trigger configuration
|
|
387
|
+
- Check for overlapping job instances
|
|
388
|
+
- Consider using `max_instances=1` for singleton jobs
|
|
389
|
+
|
|
390
|
+
### Debug Commands
|
|
391
|
+
```bash
|
|
392
|
+
# Check scheduler status
|
|
393
|
+
{{ cookiecutter.project_slug }} health check
|
|
394
|
+
|
|
395
|
+
# View scheduler logs
|
|
396
|
+
docker compose logs scheduler
|
|
397
|
+
|
|
398
|
+
# List all jobs (in Python shell)
|
|
399
|
+
python -c "
|
|
400
|
+
from app.components.scheduler.main import create_scheduler
|
|
401
|
+
scheduler = create_scheduler()
|
|
402
|
+
for job in scheduler.get_jobs():
|
|
403
|
+
print(f'{job.id}: {job.next_run_time}')
|
|
404
|
+
"
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Production Considerations
|
|
408
|
+
|
|
409
|
+
1. **Job Persistence**: Use database job store for job persistence across restarts
|
|
410
|
+
2. **Monitoring**: Set up alerts for job failures and long execution times
|
|
411
|
+
3. **Resource Limits**: Configure appropriate memory and CPU limits
|
|
412
|
+
4. **Timezone Handling**: Always use UTC for job scheduling
|
|
413
|
+
5. **Error Notifications**: Implement alerting for critical job failures
|
|
414
|
+
6. **Job Cleanup**: Regularly remove old completed jobs to prevent database bloat
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
This guide covers how to develop and maintain {{ cookiecutter.project_name }}.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
- Python 3.11+
|
|
9
|
+
- UV package manager
|
|
10
|
+
|
|
11
|
+
### Setup
|
|
12
|
+
```bash
|
|
13
|
+
# Clone and enter the project
|
|
14
|
+
cd {{ cookiecutter.project_slug }}
|
|
15
|
+
|
|
16
|
+
# Install dependencies
|
|
17
|
+
uv sync
|
|
18
|
+
|
|
19
|
+
# Copy environment template
|
|
20
|
+
cp .env.example .env
|
|
21
|
+
|
|
22
|
+
# Start development server
|
|
23
|
+
make run
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Development Commands
|
|
27
|
+
|
|
28
|
+
### Running the Application
|
|
29
|
+
```bash
|
|
30
|
+
make run # Start with Docker
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Health Monitoring
|
|
34
|
+
```bash
|
|
35
|
+
make health # Check system health
|
|
36
|
+
make health-detailed # Detailed health information
|
|
37
|
+
make health-json # JSON health output
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Code Quality
|
|
41
|
+
```bash
|
|
42
|
+
make test # Run test suite
|
|
43
|
+
make lint # Check code style
|
|
44
|
+
make typecheck # Run type checking
|
|
45
|
+
make check # Run all checks
|
|
46
|
+
make fix # Auto-fix code issues
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Documentation
|
|
50
|
+
```bash
|
|
51
|
+
make docs-serve # Serve documentation locally (http://localhost:8001)
|
|
52
|
+
make docs-build # Build static documentation
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Project Structure
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
{{ cookiecutter.project_slug }}/
|
|
59
|
+
├── app/
|
|
60
|
+
│ ├── components/ # Application components{% if cookiecutter.include_scheduler == "yes" %}
|
|
61
|
+
│ │ ├── scheduler/ # Background task scheduling{% endif %}
|
|
62
|
+
│ │ ├── backend/ # FastAPI web server
|
|
63
|
+
│ │ ├── frontend/ # Flet user interface
|
|
64
|
+
│ │ └── worker/ # Background task workers (arq)
|
|
65
|
+
│ ├── core/ # Core utilities and configuration
|
|
66
|
+
│ ├── services/ # Business logic services
|
|
67
|
+
│ └── cli/ # Command-line interface
|
|
68
|
+
├── tests/ # Test suite
|
|
69
|
+
├── docs/ # Project documentation
|
|
70
|
+
└── docker-compose.yml # Container orchestration
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Adding New Features
|
|
74
|
+
|
|
75
|
+
### 1. Create Business Logic
|
|
76
|
+
Add pure business logic functions to `app/services/`:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# app/services/my_service.py
|
|
80
|
+
async def process_data(data: str) -> str:
|
|
81
|
+
"""Process data and return result."""
|
|
82
|
+
return f"Processed: {data}"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Add API Endpoints
|
|
86
|
+
Create routes in `app/components/backend/api/`:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# app/components/backend/api/my_endpoints.py
|
|
90
|
+
from fastapi import APIRouter
|
|
91
|
+
from app.services.my_service import process_data
|
|
92
|
+
|
|
93
|
+
router = APIRouter()
|
|
94
|
+
|
|
95
|
+
@router.post("/process")
|
|
96
|
+
async def process_endpoint(data: str):
|
|
97
|
+
result = await process_data(data)
|
|
98
|
+
return {"result": result}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Register in `app/components/backend/api/routing.py`:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from app.components.backend.api import my_endpoints
|
|
105
|
+
|
|
106
|
+
def include_routers(app: FastAPI) -> None:
|
|
107
|
+
app.include_router(my_endpoints.router, prefix="/api", tags=["processing"])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 3. Add Background Tasks
|
|
111
|
+
Create worker tasks in `app/components/worker/tasks/`:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# app/components/worker/tasks/my_tasks.py
|
|
115
|
+
async def background_process_data(data: str) -> dict[str, str]:
|
|
116
|
+
"""Process data in background."""
|
|
117
|
+
logger.info(f"Processing {data} in background")
|
|
118
|
+
|
|
119
|
+
# Your processing logic here
|
|
120
|
+
result = f"Processed: {data}"
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"status": "completed",
|
|
124
|
+
"result": result,
|
|
125
|
+
"timestamp": datetime.now(UTC).isoformat()
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Register in worker queue (`app/components/worker/queues/system.py`):
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from app.components.worker.tasks.my_tasks import background_process_data
|
|
133
|
+
|
|
134
|
+
class WorkerSettings:
|
|
135
|
+
functions = [
|
|
136
|
+
system_health_check,
|
|
137
|
+
background_process_data, # Add your task here
|
|
138
|
+
]
|
|
139
|
+
```{% if cookiecutter.include_scheduler == "yes" %}
|
|
140
|
+
|
|
141
|
+
### 4. Add Scheduled Tasks
|
|
142
|
+
Add jobs to the scheduler component:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
# In app/components/scheduler/main.py
|
|
146
|
+
from app.services.my_service import process_data
|
|
147
|
+
|
|
148
|
+
# Add to create_scheduler function
|
|
149
|
+
scheduler.add_job(
|
|
150
|
+
func=lambda: process_data("scheduled"),
|
|
151
|
+
trigger="cron",
|
|
152
|
+
hour=2, # Run at 2 AM daily
|
|
153
|
+
id="daily_processing",
|
|
154
|
+
name="Daily Data Processing"
|
|
155
|
+
)
|
|
156
|
+
```{% endif %}
|
|
157
|
+
|
|
158
|
+
## Testing
|
|
159
|
+
|
|
160
|
+
### Running Tests
|
|
161
|
+
```bash
|
|
162
|
+
make test # All tests
|
|
163
|
+
make test-verbose # Verbose output
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Writing Tests
|
|
167
|
+
Create tests in the `tests/` directory:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# tests/services/test_my_service.py
|
|
171
|
+
import pytest
|
|
172
|
+
from app.services.my_service import process_data
|
|
173
|
+
|
|
174
|
+
@pytest.mark.asyncio
|
|
175
|
+
async def test_process_data():
|
|
176
|
+
result = await process_data("test")
|
|
177
|
+
assert result == "Processed: test"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Deployment
|
|
181
|
+
|
|
182
|
+
### Docker Deployment
|
|
183
|
+
```bash
|
|
184
|
+
# Build and run
|
|
185
|
+
make docker-build
|
|
186
|
+
make docker-up
|
|
187
|
+
|
|
188
|
+
# Or use profiles for specific components
|
|
189
|
+
docker compose --profile dev up
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Environment Configuration
|
|
193
|
+
Configure `.env` file for your environment:
|
|
194
|
+
|
|
195
|
+
```env
|
|
196
|
+
# API Configuration
|
|
197
|
+
API_HOST=0.0.0.0
|
|
198
|
+
API_PORT=8000
|
|
199
|
+
|
|
200
|
+
# Logging
|
|
201
|
+
LOG_LEVEL=INFO{% if cookiecutter.include_scheduler == "yes" %}
|
|
202
|
+
|
|
203
|
+
# Scheduler Configuration
|
|
204
|
+
SCHEDULER_TIMEZONE=UTC{% endif %}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Monitoring and Health Checks
|
|
208
|
+
|
|
209
|
+
{{ cookiecutter.project_name }} includes comprehensive health monitoring:
|
|
210
|
+
|
|
211
|
+
- **Health Endpoints**: `/health/` and `/health/detailed`
|
|
212
|
+
- **CLI Commands**: `{{ cookiecutter.project_slug }} health check`
|
|
213
|
+
- **Component Monitoring**: Automatic health checks for all components
|
|
214
|
+
|
|
215
|
+
See [Health Monitoring](health.md) for complete details.
|