Skip to content

Commit 63220cc

Browse files
committed
first draft of attachment presigned url generation, delete routes for updating and deleting attachments
1 parent 0b3213d commit 63220cc

File tree

3 files changed

+64
-114
lines changed

3 files changed

+64
-114
lines changed
Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import uuid
22
from typing import Any
33

4+
import boto3
45
from fastapi import APIRouter, HTTPException
6+
from fastapi.responses import RedirectResponse
57
from sqlmodel import func, select
68

7-
from app.api.deps import CurrentUser, SessionDep
9+
from app.api.deps import AwsDep, CurrentUser, SessionDep
10+
from app.core.config import Settings
811
from app.models.attachments import (
912
Attachment,
1013
AttachmentCreate,
14+
AttachmentCreatePublic,
1115
AttachmentPublic,
1216
AttachmentsPublic,
1317
AttachmentUpdate,
@@ -22,7 +26,7 @@ def read_attachments(
2226
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
2327
) -> Any:
2428
"""
25-
Retrieve attachments.
29+
Retrieve list of all attachments.
2630
"""
2731
count_statement = select(func.count()).select_from(Attachment)
2832
count = session.exec(count_statement).one()
@@ -31,64 +35,68 @@ def read_attachments(
3135

3236
return AttachmentsPublic(data=attachments, count=count)
3337

38+
@router.get("/{id}/content")
39+
def read_attachment_content(session: SessionDep, aws_client: AwsDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
40+
"""
41+
Get attachment content by ID.
42+
"""
43+
attachment = session.get(Attachment, id)
44+
if not attachment:
45+
raise HTTPException(status_code=404, detail="Attachment not found")
46+
47+
try:
48+
presigned_url = aws_client.generate_presigned_url(
49+
"get_object",
50+
Params={
51+
"Bucket": Settings.AWS_S3_ATTACHMENTS_BUCKET,
52+
"Key": attachment.storage_path,
53+
"ContentDisposition": f"attachment; filename={attachment.file_name}",
54+
},
55+
ExpiresIn=3600,
56+
)
57+
except Exception as e:
58+
raise HTTPException(status_code=500, detail="Could not generate presigned URL")
59+
60+
return RedirectResponse(status_code=302, url=presigned_url)
61+
3462

3563
@router.get("/{id}", response_model=AttachmentPublic)
3664
def read_attachment(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
3765
"""
38-
Get attachment by ID.
66+
Get attachment details by ID.
3967
"""
4068
attachment = session.get(Attachment, id)
4169
if not attachment:
4270
raise HTTPException(status_code=404, detail="Attachment not found")
4371
return attachment
4472

4573

46-
@router.post("/", response_model=AttachmentPublic)
74+
@router.post("/", response_model=AttachmentCreatePublic)
4775
def create_attachment(
48-
*, session: SessionDep, current_user: CurrentUser, attachment_in: AttachmentCreate
76+
*, session: SessionDep, aws_client: AwsDep, current_user: CurrentUser, attachment_in: AttachmentCreate
4977
) -> Any:
5078
"""
51-
Create new attachment.
79+
Create a new attachment.
5280
"""
5381
attachment = Attachment.model_validate(attachment_in)
5482
session.add(attachment)
5583
session.commit()
5684
session.refresh(attachment)
57-
return attachment
58-
5985

60-
@router.put("/{id}", response_model=AttachmentPublic)
61-
def update_attachment(
62-
*,
63-
session: SessionDep,
64-
current_user: CurrentUser,
65-
id: uuid.UUID,
66-
attachment_in: AttachmentUpdate,
67-
) -> Any:
68-
"""
69-
Update an attachment.
70-
"""
71-
attachment = session.get(Attachment, id)
72-
if not attachment:
73-
raise HTTPException(status_code=404, detail="Attachment not found")
74-
update_dict = attachment_in.model_dump(exclude_unset=True)
75-
attachment.sqlmodel_update(update_dict)
76-
session.add(attachment)
77-
session.commit()
78-
session.refresh(attachment)
79-
return attachment
86+
try:
87+
presigned_upload_url = aws_client.generate_presigned_url(
88+
"put_object",
89+
Params={
90+
"Bucket": Settings.AWS_S3_ATTACHMENTS_BUCKET,
91+
"Key": attachment.storage_path,
92+
"ContentDisposition": f"attachment; filename={attachment.file_name}",
93+
},
94+
ExpiresIn=3600,
95+
)
96+
except Exception as e:
97+
raise HTTPException(status_code=500, detail="Could not generate presigned URL")
8098

99+
resmodel = AttachmentCreatePublic.model_validate(attachment)
100+
resmodel.upload_url = presigned_upload_url
81101

82-
@router.delete("/{id}")
83-
def delete_attachment(
84-
session: SessionDep, current_user: CurrentUser, id: uuid.UUID
85-
) -> Message:
86-
"""
87-
Delete an attachment.
88-
"""
89-
attachment = session.get(Attachment, id)
90-
if not attachment:
91-
raise HTTPException(status_code=404, detail="Attachment not found")
92-
session.delete(attachment)
93-
session.commit()
94-
return Message(message="Attachment deleted successfully")
102+
return resmodel

backend/app/models/attachments.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,19 @@ class Attachment(AttachmentBase, table=True):
3838
)
3939
patient: Patient | None = Relationship(back_populates="attachments")
4040

41+
# get the blob storage path, which is assembled from the patient
42+
# id and the attachment ID
43+
@property
44+
def storage_path(self):
45+
return f"patients/{self.patient_id}/attachments/{self.id}"
46+
4147
# Properties to return via API, id is always required
4248
class AttachmentPublic(AttachmentBase):
4349
id: uuid.UUID
50+
patient_id: uuid.UUID = Field(nullable=False)
51+
52+
class AttachmentCreatePublic(AttachmentPublic):
53+
upload_url: str = Field(nullable=False) # presigned URL for uploading to blob storage
4454

4555
class AttachmentsPublic(SQLModel):
4656
data: list[AttachmentPublic]

backend/app/tests/api/routes/test_attachments.py

Lines changed: 5 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ def test_create_attachment(
3838
content = response.json()
3939
assert content["file_name"] == data["file_name"]
4040
assert content["mime_type"] == data["mime_type"]
41+
assert content["upload_url"] is not None
4142
assert "id" in content
4243

43-
def test_read_attachment(
44+
def test_read_attachment_details(
4445
client: TestClient, superuser_token_headers: Dict[str, str], db: Session
4546
) -> None:
4647
# Create test patient
@@ -73,8 +74,10 @@ def test_read_attachment(
7374
content = response.json()
7475
assert content["file_name"] == attachment.file_name
7576
assert content["id"] == str(attachment.id)
77+
assert content["patient_id"] == str(attachment.patient_id)
7678

77-
def test_read_attachments(
79+
80+
def test_list_attachments(
7881
client: TestClient, superuser_token_headers: Dict[str, str], db: Session
7982
) -> None:
8083
# Create test patient
@@ -113,77 +116,6 @@ def test_read_attachments(
113116
assert content["count"] >= 2
114117
assert len(content["data"]) >= 2
115118

116-
def test_update_attachment(
117-
client: TestClient, superuser_token_headers: Dict[str, str], db: Session
118-
) -> None:
119-
# Create test patient
120-
patient = Patient(
121-
name="Test Patient",
122-
dob=datetime(2000, 1, 1),
123-
contact_info="[email protected]"
124-
)
125-
db.add(patient)
126-
db.commit()
127-
db.refresh(patient)
128-
129-
# Create test attachment
130-
attachment = Attachment(
131-
file_name="test.pdf",
132-
mime_type="application/pdf",
133-
storage_path="patients/attachments/test.pdf",
134-
patient_id=patient.id
135-
)
136-
db.add(attachment)
137-
db.commit()
138-
db.refresh(attachment)
139-
140-
data = {"file_name": "updated.pdf"}
141-
response = client.put(
142-
f"/api/v1/attachments/{attachment.id}",
143-
headers=superuser_token_headers,
144-
json=data,
145-
)
146-
assert response.status_code == 200
147-
content = response.json()
148-
assert content["file_name"] == data["file_name"]
149-
assert content["id"] == str(attachment.id)
150-
151-
def test_delete_attachment(
152-
client: TestClient, superuser_token_headers: Dict[str, str], db: Session
153-
) -> None:
154-
# Create test patient
155-
patient = Patient(
156-
name="Test Patient",
157-
dob=datetime(2000, 1, 1),
158-
contact_info="[email protected]"
159-
)
160-
db.add(patient)
161-
db.commit()
162-
db.refresh(patient)
163-
164-
# Create test attachment
165-
attachment = Attachment(
166-
file_name="test.pdf",
167-
mime_type="application/pdf",
168-
storage_path="patients/attachments/test.pdf",
169-
patient_id=patient.id
170-
)
171-
db.add(attachment)
172-
db.commit()
173-
db.refresh(attachment)
174-
175-
response = client.delete(
176-
f"/api/v1/attachments/{attachment.id}",
177-
headers=superuser_token_headers,
178-
)
179-
assert response.status_code == 200
180-
181-
# Verify attachment was deleted
182-
response = client.get(
183-
f"/api/v1/attachments/{attachment.id}",
184-
headers=superuser_token_headers,
185-
)
186-
assert response.status_code == 404
187119

188120
def test_attachment_not_found(
189121
client: TestClient, superuser_token_headers: Dict[str, str]

0 commit comments

Comments
 (0)