Skip to content

Commit 3a79a0d

Browse files
committed
Refactor text input: service layer, repository, reuse validation handler
1 parent dbcbe95 commit 3a79a0d

5 files changed

Lines changed: 76 additions & 49 deletions

File tree

app/api/router.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from fastapi import APIRouter
22

3-
from app.api.routes import forms, jobs, system, templates, weather, zipcode
4-
from app.api.routes import input as input_routes
3+
from app.api.routes import forms, input, jobs, system, templates, weather, zipcode
54
from app.core.config import API_PREFIX
65

76
api_router = APIRouter()
@@ -11,4 +10,4 @@
1110
api_router.include_router(jobs.router, prefix=API_PREFIX)
1211
api_router.include_router(weather.router, prefix=API_PREFIX)
1312
api_router.include_router(zipcode.router, prefix=API_PREFIX)
14-
api_router.include_router(input_routes.router, prefix=API_PREFIX)
13+
api_router.include_router(input.router, prefix=API_PREFIX)

app/api/routes/input.py

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,51 @@
1-
from datetime import datetime, timezone
21
from uuid import UUID
32

43
from fastapi import APIRouter, Depends
5-
from fastapi.responses import JSONResponse
6-
from sqlmodel import Session, select
4+
from fastapi.exceptions import RequestValidationError
5+
from sqlmodel import Session
76

87
from app.api.deps import get_db
9-
from app.api.schemas.enums import InputStatus, InputType
8+
from app.api.schemas.enums import InputStatus
109
from app.api.schemas.input import InputRecordResponse, TextInputRequest, TextInputResponse
1110
from app.core.config import INPUT_POLL_INTERVAL_SECONDS
1211
from app.core.errors.base import AppError
13-
from app.models import Input
12+
from app.db.repositories import create_input, get_input as repo_get_input
13+
from app.services.input import InputService
1414

1515
router = APIRouter(prefix="/input", tags=["input"])
1616

1717

1818
@router.post("/text", response_model=TextInputResponse, status_code=201)
1919
def submit_text_input(body: TextInputRequest, db: Session = Depends(get_db)):
20-
narrative = body.narrative
21-
22-
if len(narrative) > 50_000:
20+
if len(body.narrative) > 50_000:
2321
raise AppError(
2422
"Narrative exceeds maximum length of 50,000 characters",
2523
status_code=413,
2624
error_code="NARRATIVE_TOO_LONG",
27-
detail={"max_characters": 50_000, "received_characters": len(narrative)},
25+
detail={"max_characters": 50_000, "received_characters": len(body.narrative)},
2826
)
2927

30-
words = narrative.split()
31-
if len(words) < 10:
32-
return JSONResponse(
33-
status_code=422,
34-
content={
35-
"error_code": "VALIDATION_ERROR",
36-
"message": "Request validation failed",
37-
"validation_errors": [
38-
{
39-
"field": "narrative",
40-
"issue": "Must contain at least 10 words",
41-
"value": narrative,
42-
}
43-
],
44-
},
28+
svc = InputService()
29+
try:
30+
record = svc.build_text_input(
31+
narrative=body.narrative,
32+
station_id=body.station_id,
33+
responder_badge=body.responder_badge,
34+
incident_date_hint=body.incident_date_hint,
35+
)
36+
except ValueError as exc:
37+
raise RequestValidationError(
38+
errors=[
39+
{
40+
"loc": ("body", "narrative"),
41+
"msg": str(exc),
42+
"input": body.narrative,
43+
"type": "value_error",
44+
}
45+
]
4546
)
4647

47-
now = datetime.now(timezone.utc)
48-
record = Input(
49-
input_type=InputType.text,
50-
status=InputStatus.ready,
51-
transcript=narrative,
52-
character_count=len(narrative),
53-
word_count=len(words),
54-
station_id=body.station_id,
55-
responder_badge=body.responder_badge,
56-
incident_date_hint=body.incident_date_hint,
57-
created_at=now,
58-
updated_at=now,
59-
)
60-
db.add(record)
61-
db.commit()
62-
db.refresh(record)
48+
record = create_input(db, record)
6349

6450
return TextInputResponse(
6551
input_id=record.input_id,
@@ -73,7 +59,7 @@ def submit_text_input(body: TextInputRequest, db: Session = Depends(get_db)):
7359

7460
@router.get("/{input_id}", response_model=InputRecordResponse)
7561
def get_input(input_id: UUID, db: Session = Depends(get_db)):
76-
record = db.exec(select(Input).where(Input.input_id == input_id)).first()
62+
record = repo_get_input(db, input_id)
7763
if record is None:
7864
raise AppError(
7965
f"Input with ID {input_id} not found",

app/api/schemas/input.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,19 @@
33
from datetime import date, datetime
44
from uuid import UUID
55

6-
from pydantic import BaseModel, ConfigDict, Field
6+
from pydantic import BaseModel, Field
77

88
from app.api.schemas.enums import InputStatus, InputType
99

1010

1111
class TextInputRequest(BaseModel):
12-
model_config = ConfigDict()
1312
narrative: str = Field(min_length=20)
1413
station_id: str | None = None
1514
responder_badge: str | None = None
1615
incident_date_hint: date | None = None
1716

1817

1918
class TextInputResponse(BaseModel):
20-
model_config = ConfigDict()
2119
input_id: UUID
2220
status: InputStatus
2321
input_type: InputType
@@ -27,7 +25,6 @@ class TextInputResponse(BaseModel):
2725

2826

2927
class InputRecordResponse(BaseModel):
30-
model_config = ConfigDict()
3128
input_id: UUID
3229
input_type: InputType
3330
status: InputStatus

app/db/repositories.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from uuid import UUID
2+
13
from sqlmodel import Session, select
2-
from app.models import Template, FormSubmission, Job
4+
5+
from app.models import Template, FormSubmission, Job, Input
36

47
# Templates
58
def create_template(session: Session, template: Template) -> Template:
@@ -66,3 +69,15 @@ def delete_form_submission(session: Session, submission: FormSubmission) -> None
6669
session.delete(submission)
6770
session.commit()
6871

72+
73+
# Inputs
74+
def create_input(session: Session, input_obj: Input) -> Input:
75+
session.add(input_obj)
76+
session.commit()
77+
session.refresh(input_obj)
78+
return input_obj
79+
80+
81+
def get_input(session: Session, input_id: UUID) -> Input | None:
82+
return session.get(Input, input_id)
83+

app/services/input.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from datetime import date, datetime, timezone
2+
3+
from app.api.schemas.enums import InputStatus, InputType
4+
from app.models import Input
5+
6+
7+
class InputService:
8+
def build_text_input(
9+
self,
10+
narrative: str,
11+
station_id: str | None = None,
12+
responder_badge: str | None = None,
13+
incident_date_hint: date | None = None,
14+
) -> Input:
15+
words = narrative.split()
16+
if len(words) < 10:
17+
raise ValueError("Must contain at least 10 words")
18+
now = datetime.now(timezone.utc)
19+
return Input(
20+
input_type=InputType.text,
21+
status=InputStatus.ready,
22+
transcript=narrative,
23+
character_count=len(narrative),
24+
word_count=len(words),
25+
station_id=station_id,
26+
responder_badge=responder_badge,
27+
incident_date_hint=incident_date_hint,
28+
created_at=now,
29+
updated_at=now,
30+
)

0 commit comments

Comments
 (0)