Skip to content

Commit 4c419f7

Browse files
authored
feat: type checking with mypy in strict mode (#39)
* mypy strict mode enabled * fixes for seed_users, tracing/utils * asynciomotor/db fixes (preciser types) * type: ignored fixes ruff formatting arg-type fixes * type: ignored fixes * attr-defined fixes * type-arg fixes * backend tests: added rs flag to see the reason for tests skipped * changed positions of endpoints so that {event_id} wont catch export endpoints
1 parent a957854 commit 4c419f7

File tree

192 files changed

+4729
-6853
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

192 files changed

+4729
-6853
lines changed

.github/workflows/backend-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797
SCHEMA_SUBJECT_PREFIX: "ci.${{ github.run_id }}."
9898
run: |
9999
cd backend
100-
uv run pytest tests/integration -v --cov=app --cov-branch --cov-report=xml --cov-report=term
100+
uv run pytest tests/integration -v -rs --cov=app --cov-branch --cov-report=xml --cov-report=term
101101
102102
- name: Upload coverage to Codecov
103103
uses: codecov/codecov-action@v5

.github/workflows/mypy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
SECRET_KEY: ${{ secrets.TEST_SECRET_KEY }}
3232
run: |
3333
cd backend
34-
uv run mypy --config-file pyproject.toml .
34+
uv run mypy --config-file pyproject.toml --strict .

backend/app/api/dependencies.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,12 @@
77

88

99
@inject
10-
async def current_user(
11-
request: Request,
12-
auth_service: FromDishka[AuthService]
13-
) -> UserResponse:
10+
async def current_user(request: Request, auth_service: FromDishka[AuthService]) -> UserResponse:
1411
"""Get authenticated user."""
1512
return await auth_service.get_current_user(request)
1613

1714

1815
@inject
19-
async def admin_user(
20-
request: Request,
21-
auth_service: FromDishka[AuthService]
22-
) -> UserResponse:
16+
async def admin_user(request: Request, auth_service: FromDishka[AuthService]) -> UserResponse:
2317
"""Get authenticated admin user."""
2418
return await auth_service.get_admin(request)

backend/app/api/routes/admin/events.py

Lines changed: 75 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,12 @@
3333
from app.services.admin import AdminEventsService
3434

3535
router = APIRouter(
36-
prefix="/admin/events",
37-
tags=["admin-events"],
38-
route_class=DishkaRoute,
39-
dependencies=[Depends(admin_user)]
36+
prefix="/admin/events", tags=["admin-events"], route_class=DishkaRoute, dependencies=[Depends(admin_user)]
4037
)
4138

4239

4340
@router.post("/browse")
44-
async def browse_events(
45-
request: EventBrowseRequest,
46-
service: FromDishka[AdminEventsService]
47-
) -> EventBrowseResponse:
41+
async def browse_events(request: EventBrowseRequest, service: FromDishka[AdminEventsService]) -> EventBrowseResponse:
4842
try:
4943
event_filter = EventFilterMapper.from_admin_pydantic(request.filters)
5044

@@ -53,15 +47,15 @@ async def browse_events(
5347
skip=request.skip,
5448
limit=request.limit,
5549
sort_by=request.sort_by,
56-
sort_order=request.sort_order
50+
sort_order=request.sort_order,
5751
)
5852

5953
event_mapper = EventMapper()
6054
return EventBrowseResponse(
6155
events=[jsonable_encoder(event_mapper.to_dict(event)) for event in result.events],
6256
total=result.total,
6357
skip=result.skip,
64-
limit=result.limit
58+
limit=result.limit,
6559
)
6660

6761
except Exception as e:
@@ -70,8 +64,8 @@ async def browse_events(
7064

7165
@router.get("/stats")
7266
async def get_event_stats(
73-
service: FromDishka[AdminEventsService],
74-
hours: int = Query(default=24, le=168),
67+
service: FromDishka[AdminEventsService],
68+
hours: int = Query(default=24, le=168),
7569
) -> EventStatsResponse:
7670
try:
7771
stats = await service.get_event_stats(hours=hours)
@@ -82,11 +76,71 @@ async def get_event_stats(
8276
raise HTTPException(status_code=500, detail=str(e))
8377

8478

79+
@router.get("/export/csv")
80+
async def export_events_csv(
81+
service: FromDishka[AdminEventsService],
82+
event_types: list[EventType] | None = Query(None, description="Event types (repeat param for multiple)"),
83+
start_time: datetime | None = Query(None, description="Start time"),
84+
end_time: datetime | None = Query(None, description="End time"),
85+
limit: int = Query(default=10000, le=50000),
86+
) -> StreamingResponse:
87+
try:
88+
export_filter = EventFilterMapper.from_admin_pydantic(
89+
AdminEventFilter(
90+
event_types=event_types,
91+
start_time=start_time,
92+
end_time=end_time,
93+
)
94+
)
95+
result = await service.export_events_csv_content(filter=export_filter, limit=limit)
96+
return StreamingResponse(
97+
iter([result.content]),
98+
media_type=result.media_type,
99+
headers={"Content-Disposition": f"attachment; filename={result.file_name}"},
100+
)
101+
102+
except Exception as e:
103+
raise HTTPException(status_code=500, detail=str(e))
104+
105+
106+
@router.get("/export/json")
107+
async def export_events_json(
108+
service: FromDishka[AdminEventsService],
109+
event_types: list[EventType] | None = Query(None, description="Event types (repeat param for multiple)"),
110+
aggregate_id: str | None = Query(None, description="Aggregate ID filter"),
111+
correlation_id: str | None = Query(None, description="Correlation ID filter"),
112+
user_id: str | None = Query(None, description="User ID filter"),
113+
service_name: str | None = Query(None, description="Service name filter"),
114+
start_time: datetime | None = Query(None, description="Start time"),
115+
end_time: datetime | None = Query(None, description="End time"),
116+
limit: int = Query(default=10000, le=50000),
117+
) -> StreamingResponse:
118+
"""Export events as JSON with comprehensive filtering."""
119+
try:
120+
export_filter = EventFilterMapper.from_admin_pydantic(
121+
AdminEventFilter(
122+
event_types=event_types,
123+
aggregate_id=aggregate_id,
124+
correlation_id=correlation_id,
125+
user_id=user_id,
126+
service_name=service_name,
127+
start_time=start_time,
128+
end_time=end_time,
129+
)
130+
)
131+
result = await service.export_events_json_content(filter=export_filter, limit=limit)
132+
return StreamingResponse(
133+
iter([result.content]),
134+
media_type=result.media_type,
135+
headers={"Content-Disposition": f"attachment; filename={result.file_name}"},
136+
)
137+
138+
except Exception as e:
139+
raise HTTPException(status_code=500, detail=str(e))
140+
141+
85142
@router.get("/{event_id}")
86-
async def get_event_detail(
87-
event_id: str,
88-
service: FromDishka[AdminEventsService]
89-
) -> EventDetailResponse:
143+
async def get_event_detail(event_id: str, service: FromDishka[AdminEventsService]) -> EventDetailResponse:
90144
try:
91145
result = await service.get_event_detail(event_id)
92146

@@ -98,7 +152,7 @@ async def get_event_detail(
98152
return EventDetailResponse(
99153
event=serialized_result["event"],
100154
related_events=serialized_result["related_events"],
101-
timeline=serialized_result["timeline"]
155+
timeline=serialized_result["timeline"],
102156
)
103157

104158
except HTTPException:
@@ -109,9 +163,7 @@ async def get_event_detail(
109163

110164
@router.post("/replay")
111165
async def replay_events(
112-
request: EventReplayRequest,
113-
background_tasks: BackgroundTasks,
114-
service: FromDishka[AdminEventsService]
166+
request: EventReplayRequest, background_tasks: BackgroundTasks, service: FromDishka[AdminEventsService]
115167
) -> EventReplayResponse:
116168
try:
117169
replay_correlation_id = f"replay_{CorrelationContext.get_correlation_id()}"
@@ -150,10 +202,7 @@ async def replay_events(
150202

151203

152204
@router.get("/replay/{session_id}/status")
153-
async def get_replay_status(
154-
session_id: str,
155-
service: FromDishka[AdminEventsService]
156-
) -> EventReplayStatusResponse:
205+
async def get_replay_status(session_id: str, service: FromDishka[AdminEventsService]) -> EventReplayStatusResponse:
157206
try:
158207
status = await service.get_replay_status(session_id)
159208

@@ -171,84 +220,16 @@ async def get_replay_status(
171220

172221
@router.delete("/{event_id}")
173222
async def delete_event(
174-
event_id: str,
175-
admin: Annotated[UserResponse, Depends(admin_user)],
176-
service: FromDishka[AdminEventsService]
223+
event_id: str, admin: Annotated[UserResponse, Depends(admin_user)], service: FromDishka[AdminEventsService]
177224
) -> EventDeleteResponse:
178225
try:
179226
deleted = await service.delete_event(event_id=event_id, deleted_by=admin.email)
180227
if not deleted:
181228
raise HTTPException(status_code=500, detail="Failed to delete event")
182229

183-
return EventDeleteResponse(
184-
message="Event deleted and archived",
185-
event_id=event_id
186-
)
230+
return EventDeleteResponse(message="Event deleted and archived", event_id=event_id)
187231

188232
except HTTPException:
189233
raise
190234
except Exception as e:
191235
raise HTTPException(status_code=500, detail=str(e))
192-
193-
194-
@router.get("/export/csv")
195-
async def export_events_csv(
196-
service: FromDishka[AdminEventsService],
197-
event_types: list[EventType] | None = Query(None, description="Event types (repeat param for multiple)"),
198-
start_time: datetime | None = Query(None, description="Start time"),
199-
end_time: datetime | None = Query(None, description="End time"),
200-
limit: int = Query(default=10000, le=50000),
201-
) -> StreamingResponse:
202-
try:
203-
export_filter = EventFilterMapper.from_admin_pydantic(
204-
AdminEventFilter(
205-
event_types=event_types,
206-
start_time=start_time,
207-
end_time=end_time,
208-
)
209-
)
210-
result = await service.export_events_csv_content(filter=export_filter, limit=limit)
211-
return StreamingResponse(
212-
iter([result.content]),
213-
media_type=result.media_type,
214-
headers={"Content-Disposition": f"attachment; filename={result.filename}"},
215-
)
216-
217-
except Exception as e:
218-
raise HTTPException(status_code=500, detail=str(e))
219-
220-
221-
@router.get("/export/json")
222-
async def export_events_json(
223-
service: FromDishka[AdminEventsService],
224-
event_types: list[EventType] | None = Query(None, description="Event types (repeat param for multiple)"),
225-
aggregate_id: str | None = Query(None, description="Aggregate ID filter"),
226-
correlation_id: str | None = Query(None, description="Correlation ID filter"),
227-
user_id: str | None = Query(None, description="User ID filter"),
228-
service_name: str | None = Query(None, description="Service name filter"),
229-
start_time: datetime | None = Query(None, description="Start time"),
230-
end_time: datetime | None = Query(None, description="End time"),
231-
limit: int = Query(default=10000, le=50000),
232-
) -> StreamingResponse:
233-
"""Export events as JSON with comprehensive filtering."""
234-
try:
235-
export_filter = EventFilterMapper.from_admin_pydantic(
236-
AdminEventFilter(
237-
event_types=event_types,
238-
aggregate_id=aggregate_id,
239-
correlation_id=correlation_id,
240-
user_id=user_id,
241-
service_name=service_name,
242-
start_time=start_time,
243-
end_time=end_time,
244-
)
245-
)
246-
result = await service.export_events_json_content(filter=export_filter, limit=limit)
247-
return StreamingResponse(
248-
iter([result.content]),
249-
media_type=result.media_type,
250-
headers={"Content-Disposition": f"attachment; filename={result.filename}"},
251-
)
252-
253-
except Exception as e:
254-
raise HTTPException(status_code=500, detail=str(e))

backend/app/api/routes/admin/settings.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,14 @@
1212
from app.services.admin import AdminSettingsService
1313

1414
router = APIRouter(
15-
prefix="/admin/settings",
16-
tags=["admin", "settings"],
17-
route_class=DishkaRoute,
18-
dependencies=[Depends(admin_user)]
15+
prefix="/admin/settings", tags=["admin", "settings"], route_class=DishkaRoute, dependencies=[Depends(admin_user)]
1916
)
2017

2118

2219
@router.get("/", response_model=SystemSettings)
2320
async def get_system_settings(
24-
admin: Annotated[UserResponse, Depends(admin_user)],
25-
service: FromDishka[AdminSettingsService],
21+
admin: Annotated[UserResponse, Depends(admin_user)],
22+
service: FromDishka[AdminSettingsService],
2623
) -> SystemSettings:
2724
try:
2825
domain_settings = await service.get_system_settings(admin.username)
@@ -35,18 +32,15 @@ async def get_system_settings(
3532

3633
@router.put("/", response_model=SystemSettings)
3734
async def update_system_settings(
38-
admin: Annotated[UserResponse, Depends(admin_user)],
39-
settings: SystemSettings,
40-
service: FromDishka[AdminSettingsService],
35+
admin: Annotated[UserResponse, Depends(admin_user)],
36+
settings: SystemSettings,
37+
service: FromDishka[AdminSettingsService],
4138
) -> SystemSettings:
4239
try:
4340
settings_mapper = SettingsMapper()
4441
domain_settings = settings_mapper.system_settings_from_pydantic(settings.model_dump())
4542
except (ValueError, ValidationError, KeyError) as e:
46-
raise HTTPException(
47-
status_code=422,
48-
detail=f"Invalid settings: {str(e)}"
49-
)
43+
raise HTTPException(status_code=422, detail=f"Invalid settings: {str(e)}")
5044
except Exception:
5145
raise HTTPException(status_code=400, detail="Invalid settings format")
5246

@@ -68,8 +62,8 @@ async def update_system_settings(
6862

6963
@router.post("/reset", response_model=SystemSettings)
7064
async def reset_system_settings(
71-
admin: Annotated[UserResponse, Depends(admin_user)],
72-
service: FromDishka[AdminSettingsService],
65+
admin: Annotated[UserResponse, Depends(admin_user)],
66+
service: FromDishka[AdminSettingsService],
7367
) -> SystemSettings:
7468
try:
7569
reset_domain_settings = await service.reset_system_settings(admin.username, admin.user_id)

0 commit comments

Comments
 (0)