Skip to content

Commit 55eadba

Browse files
committed
refactor: refactor job routes and fix after e2e tests
1 parent 225dc01 commit 55eadba

20 files changed

Lines changed: 850 additions & 810 deletions

specs/admin-refresh-all/DOCS.md

Lines changed: 120 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
# Admin Refresh All — Architecture Docs
22

3-
Visual overview of the jobs API after the admin refresh feature was added. Shows the existing per-job route, the new admin refresh route, the shared services they reuse, and how a single admin trigger fans out into seven child job runs.
3+
Visual overview of the jobs API after the admin refresh feature was added **and refactored**. Shows the existing per-job route, the new admin refresh route, the shared services they reuse, how a single admin trigger fans out into seven child job runs, and the clean-code improvements applied in the post-feature refactor.
4+
5+
---
6+
7+
## Refactor summary (post-feature)
8+
9+
Six clean-code improvements applied after the initial feature ship — all non-breaking:
10+
11+
| Area | What changed |
12+
|---|---|
13+
| `services/jobs/base.py` | New `BaseJob` ABC: all 4 job classes extend it; lifecycle (STARTED + FAILURE) is no longer duplicated |
14+
| `services/jobs/__init__.py` | New `jobs` subpackage exposes `BaseJob` at a single canonical import path |
15+
| `services/full_impact_refresh_service.py` | `FullImpactRefreshOrchestrator` class groups orchestration helpers; module-level shims keep all imports backward-compatible |
16+
| `schemas/job_run.py` + `schemas/full_impact_refresh.py` | `schemas/job.py` split into two focused modules; original file is now a compatibility shim |
17+
| `utils/time.py` | `utc_now()` helper replaces deprecated `datetime.utcnow()` calls across all job and service files |
18+
| `utils/exceptions.py` | `@translate_errors` decorator converts unhandled exceptions to HTTP 500 on the three `router.*` handlers |
419

520
## 1. Component overview
621

7-
Highlights what is reused vs. newly introduced. New components added by this feature are tagged `NEW`.
22+
Highlights what is reused vs. newly introduced. Components added by the feature are tagged `NEW`; refactored components are tagged `REFACTOR`.
823

924
```mermaid
1025
flowchart LR
@@ -24,6 +39,7 @@ flowchart LR
2439
Allowlist["enforce_ip_allowlist<br/>NEW"]
2540
RateLimit["check_rate_limit<br/>NEW"]
2641
Idempotency["validate_idempotency_key<br/>NEW"]
42+
TranslateErrors["@translate_errors decorator<br/>REFACTOR"]
2743
end
2844
2945
subgraph Routes[Route handlers]
@@ -36,19 +52,31 @@ flowchart LR
3652
3753
subgraph Services[Services]
3854
DispatchHelpers["job_dispatch_service<br/>resolve_actual_job_name<br/>execute_job_in_background<br/>NEW module"]
39-
RefreshService["full_impact_refresh_service<br/>build_dispatch_plan<br/>dispatch_full_refresh<br/>build_*_response<br/>NEW"]
55+
RefreshService["full_impact_refresh_service<br/>FullImpactRefreshOrchestrator CLASS<br/>+ module-level shims<br/>REFACTOR"]
4056
Registry[jobs.JOB_REGISTRY<br/>get_job_class]
41-
KpiJob[KpiMeasuresAnalysisJob]
42-
QuantJob[McdaQuantitativeJob]
43-
QualJob[McdaQualitativeJob]
44-
CustomJob[McdaCustomJob]
57+
BaseJob["BaseJob ABC<br/>run → _execute<br/>REFACTOR"]
58+
KpiJob["KpiMeasuresAnalysisJob<br/>extends BaseJob"]
59+
QuantJob["McdaQuantitativeJob<br/>extends BaseJob"]
60+
QualJob["McdaQualitativeJob<br/>extends BaseJob"]
61+
CustomJob["McdaCustomJob<br/>extends BaseJob"]
4562
end
4663
4764
subgraph Persistence
4865
JobRepo["JobRepository<br/>+ get_in_progress_full_refresh NEW<br/>+ update_dispatched_job NEW"]
4966
JobRunsTable[(job_runs table)]
5067
end
5168
69+
subgraph Utils[Utilities]
70+
UtcNow["utils/time.py<br/>utc_now()<br/>REFACTOR"]
71+
Exceptions["utils/exceptions.py<br/>@translate_errors<br/>REFACTOR"]
72+
end
73+
74+
subgraph Schemas[Schemas]
75+
JobRunSchema["schemas/job_run.py<br/>JobNameEnum · JobStatusEnum<br/>TriggerJobRequest · JobRunResponse<br/>REFACTOR"]
76+
RefreshSchema["schemas/full_impact_refresh.py<br/>FullRefreshDispatchedJob<br/>FullImpactRefresh*Response<br/>REFACTOR"]
77+
JobShim["schemas/job.py<br/>← compatibility shim<br/>REFACTOR"]
78+
end
79+
5280
Config["settings.JOB_RUN_CONFIGURATION<br/>NEW"]
5381
5482
Backoffice -->|X-Admin-Refresh-Key| AdminRouter
@@ -64,6 +92,10 @@ flowchart LR
6492
JobsRouter --> GetJob
6593
JobsRouter --> ListJobs
6694
95+
TriggerJob --> TranslateErrors
96+
GetJob --> TranslateErrors
97+
ListJobs --> TranslateErrors
98+
6799
TriggerRefresh --> RateLimit
68100
TriggerRefresh --> Idempotency
69101
@@ -87,30 +119,47 @@ flowchart LR
87119
Registry --> QualJob
88120
Registry --> CustomJob
89121
122+
KpiJob --> BaseJob
123+
QuantJob --> BaseJob
124+
QualJob --> BaseJob
125+
CustomJob --> BaseJob
126+
127+
BaseJob --> UtcNow
128+
RefreshService --> UtcNow
129+
TriggerRefresh --> UtcNow
130+
131+
TranslateErrors --> Exceptions
132+
133+
JobShim --> JobRunSchema
134+
JobShim --> RefreshSchema
135+
90136
JobRepo --> JobRunsTable
91137
```
92138

93139
## 2. Existing per-job trigger flow
94140

95-
`POST /jobs/runs/{job_name}` — unchanged behavior, now using the extracted `resolve_actual_job_name` and `execute_job_in_background` helpers from `job_dispatch_service`.
141+
`POST /jobs/runs/{job_name}` — unchanged behavior, now using the extracted `resolve_actual_job_name` and `execute_job_in_background` helpers from `job_dispatch_service`. Route handlers are now wrapped by `@translate_errors` (refactored).
96142

97143
```mermaid
98144
sequenceDiagram
99145
autonumber
100146
participant Client
101147
participant Router as jobs.router
102148
participant Verify as verify_api_key
149+
participant TE as @translate_errors
103150
participant Handler as trigger_job
104151
participant Dispatch as job_dispatch_service
105152
participant Repo as JobRepository
106153
participant DB as job_runs table
107154
participant BG as FastAPI BackgroundTasks
108-
participant Job as JobClass.run
155+
participant Base as BaseJob.run
156+
participant Job as JobClass._execute
109157
110158
Client->>Router: POST /jobs/runs/{job_name}<br/>X-Internal-API-Key
111159
Router->>Verify: verify_api_key(headers)
112160
Verify-->>Router: ok
113-
Router->>Handler: trigger_job(job_name, params)
161+
Router->>TE: @translate_errors wraps handler
162+
TE->>Handler: trigger_job(job_name, params)
114163
Handler->>Dispatch: resolve_actual_job_name(job_name, params)
115164
Dispatch-->>Handler: actual_job_name
116165
Handler->>Repo: create_job_run(actual_job_name)
@@ -120,8 +169,11 @@ sequenceDiagram
120169
Handler->>BG: add_task(execute_job_in_background, job_name, job_id, params)
121170
Handler-->>Client: 201 JobRunResponse
122171
BG->>Dispatch: execute_job_in_background(job_name, job_id, params)
123-
Dispatch->>Job: JobClass.run(job_id, db, params)
124-
Job->>Repo: update_job_status / update_job_data
172+
Dispatch->>Base: JobClass.run(job_id, db, params)
173+
Base->>Repo: update_job_status(STARTED)
174+
Base->>Job: cls._execute(job_id, db, params, job_repo)
175+
Job-->>Base: success / exception
176+
Base->>Repo: update_job_status(SUCCESS or FAILURE)
125177
Repo->>DB: UPDATE job_run
126178
```
127179

@@ -211,7 +263,7 @@ sequenceDiagram
211263

212264
## 5. Class-level interactions
213265

214-
How the new modules collaborate with the existing job classes and repository. Reuse is intentional: the admin orchestration calls the same dispatch helpers as the per-job route.
266+
How the new modules collaborate with the existing job classes and repository. Reuse is intentional: the admin orchestration calls the same dispatch helpers as the per-job route. Job lifecycle management is now centralised in `BaseJob` (refactored).
215267

216268
```mermaid
217269
classDiagram
@@ -225,18 +277,24 @@ classDiagram
225277
+trigger_full_impact_refresh(headers, body)
226278
+get_full_impact_refresh_status(run_id)
227279
}
280+
class TranslateErrors {
281+
<<REFACTOR decorator>>
282+
+__call__(func) wraps route handlers
283+
re-raises HTTPException
284+
converts other exceptions → HTTP 500
285+
}
228286
class JobDispatchService {
229287
<<NEW module>>
230288
+resolve_actual_job_name(job_name, params)
231289
+execute_job_in_background(job_name, job_id, params)
232290
}
233-
class FullImpactRefreshService {
234-
<<NEW>>
235-
+build_dispatch_plan(started_at)
236-
+build_initial_dispatch_state(plan)
237-
+dispatch_full_refresh(parent_run_id, plan)
238-
+build_trigger_response(plan, run_id, started_at)
239-
+build_status_response(parent_run, repo)
291+
class FullImpactRefreshOrchestrator {
292+
<<REFACTOR class>>
293+
build_dispatch_plan : staticmethod
294+
build_initial_dispatch_state : staticmethod
295+
dispatch : staticmethod
296+
build_trigger_response : staticmethod
297+
build_status_response : staticmethod
240298
}
241299
class AdminRefreshGuards {
242300
<<NEW>>
@@ -250,6 +308,10 @@ classDiagram
250308
+verify_api_key(header)
251309
+verify_admin_refresh_api_key(header) <<NEW>>
252310
}
311+
class TimeUtils {
312+
<<REFACTOR utils/time.py>>
313+
+utc_now() datetime
314+
}
253315
class JobRepository {
254316
+create_job_run(job_name)
255317
+get_job_run(job_id)
@@ -259,14 +321,32 @@ classDiagram
259321
+get_in_progress_full_refresh() <<NEW>>
260322
+update_dispatched_job(parent_id, sequence, updates) <<NEW>>
261323
}
324+
class BaseJob {
325+
<<REFACTOR ABC services/jobs/base.py>>
326+
+run(job_id, db, params)$ classmethod
327+
+_execute(job_id, db, params, job_repo)$ abstract classmethod
328+
lifecycle: STARTED → SUCCESS/FAILURE
329+
}
330+
class KpiMeasuresAnalysisJob {
331+
<<extends BaseJob>>
332+
+_execute(job_id, db, params, job_repo)$
333+
}
334+
class McdaQuantitativeJob {
335+
<<extends BaseJob>>
336+
+_execute(job_id, db, params, job_repo)$
337+
}
338+
class McdaQualitativeJob {
339+
<<extends BaseJob>>
340+
+_execute(job_id, db, params, job_repo)$
341+
}
342+
class McdaCustomJob {
343+
<<extends BaseJob>>
344+
+_execute(job_id, db, params, job_repo)$
345+
}
262346
class JobRegistry {
263347
+JOB_REGISTRY: Dict
264348
+get_job_class(job_name)
265349
}
266-
class KpiMeasuresAnalysisJob
267-
class McdaQuantitativeJob
268-
class McdaQualitativeJob
269-
class McdaCustomJob
270350
class Settings {
271351
+ADMIN_REFRESH_API_KEY <<NEW>>
272352
+ADMIN_REFRESH_ALLOWED_IPS <<NEW>>
@@ -276,24 +356,36 @@ classDiagram
276356
+JOB_RUN_CONFIGURATION <<NEW>>
277357
}
278358
359+
JobsRouter --> TranslateErrors : decorates handlers
279360
JobsRouter --> AuthDependencies : verify_api_key
280361
JobsRouter --> JobDispatchService : resolve + execute
281362
JobsRouter --> JobRepository
363+
JobsRouter --> TimeUtils : utc_now()
282364
283365
AdminRouter --> AuthDependencies : verify_admin_refresh_api_key
284366
AdminRouter --> AdminRefreshGuards
285-
AdminRouter --> FullImpactRefreshService
367+
AdminRouter --> FullImpactRefreshOrchestrator
286368
AdminRouter --> JobRepository
369+
AdminRouter --> TimeUtils : utc_now()
287370
288-
FullImpactRefreshService --> Settings : JOB_RUN_CONFIGURATION
289-
FullImpactRefreshService --> JobDispatchService
290-
FullImpactRefreshService --> JobRepository
371+
FullImpactRefreshOrchestrator --> Settings : JOB_RUN_CONFIGURATION
372+
FullImpactRefreshOrchestrator --> JobDispatchService
373+
FullImpactRefreshOrchestrator --> JobRepository
374+
FullImpactRefreshOrchestrator --> TimeUtils : utc_now()
291375
292376
JobDispatchService --> JobRegistry
293377
JobRegistry --> KpiMeasuresAnalysisJob
294378
JobRegistry --> McdaQuantitativeJob
295379
JobRegistry --> McdaQualitativeJob
296380
JobRegistry --> McdaCustomJob
381+
382+
KpiMeasuresAnalysisJob --|> BaseJob
383+
McdaQuantitativeJob --|> BaseJob
384+
McdaQualitativeJob --|> BaseJob
385+
McdaCustomJob --|> BaseJob
386+
387+
BaseJob --> JobRepository : instantiates
388+
BaseJob --> TimeUtils : utc_now()
297389
```
298390

299391
## 6. Persistence model

src/sum_impact_assessment/api/dependencies/auth.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@
66
from ...config.settings import settings
77

88

9-
api_key_header = APIKeyHeader(name="X-Internal-API-Key", auto_error=False)
10-
admin_refresh_api_key_header = APIKeyHeader(name="X-Admin-Refresh-Key", auto_error=False)
9+
api_key_header = APIKeyHeader(
10+
name="X-Internal-API-Key",
11+
scheme_name="InternalApiKey",
12+
auto_error=False,
13+
)
14+
admin_refresh_api_key_header = APIKeyHeader(
15+
name="X-Admin-Refresh-Key",
16+
scheme_name="AdminRefreshApiKey",
17+
auto_error=False,
18+
)
1119

1220

1321
async def verify_api_key(api_key: str = Security(api_key_header)) -> str:

0 commit comments

Comments
 (0)