Skip to content

Commit cd4fdbe

Browse files
committed
feat: implement POST endpoints for asset
1 parent c8fdc3e commit cd4fdbe

3 files changed

Lines changed: 113 additions & 74 deletions

File tree

api/asset.py

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
from fastapi import APIRouter, Depends, UploadFile, File
1818
from fastapi_pagination.ext.sqlalchemy import paginate
1919
from sqlalchemy import select
20-
from starlette.status import HTTP_201_CREATED
20+
from sqlalchemy.exc import ProgrammingError
21+
from starlette.status import HTTP_201_CREATED, HTTP_409_CONFLICT
2122

2223
from api.pagination import CustomPage
2324
from core.dependencies import (
@@ -39,12 +40,42 @@
3940
check_asset_exists,
4041
add_signed_url,
4142
)
43+
from services.exceptions_helper import PydanticStyleException
4244

4345
router = APIRouter(
4446
prefix="/asset", tags=["asset"], dependencies=[Depends(viewer_function)]
4547
)
4648

4749

50+
def database_error_handler(payload: CreateAsset, error: ProgrammingError) -> None:
51+
"""
52+
Handle errors raised by the database when adding or updating a sample.
53+
"""
54+
55+
error_message = error.orig.args[0]["M"]
56+
print(error_message)
57+
58+
if (
59+
error_message
60+
== 'null value in column "thing_id" of relation "asset_thing_association" violates not-null constraint'
61+
):
62+
"""
63+
Developer's notes
64+
65+
this error occurs because the thing_id is set by the Thing record that
66+
is retrieved, so if there is no Thing with thing_id it tries to set
67+
thing_id to None in the AssetThingAssociation table
68+
"""
69+
detail = {
70+
"loc": ["body", "thing_id"],
71+
"msg": f"Thing with ID {payload.thing_id} not found.",
72+
"type": "value_error",
73+
"input": {"thing_id": payload.thing_id},
74+
}
75+
76+
raise PydanticStyleException(status_code=HTTP_409_CONFLICT, detail=[detail])
77+
78+
4879
# ======= Create =========
4980
@router.post(
5081
"/upload", status_code=HTTP_201_CREATED, dependencies=[Depends(admin_function)]
@@ -64,33 +95,38 @@ async def add_asset(
6495
user: admin_dependency, session: session_dependency, asset_data: CreateAsset
6596
) -> AssetResponse:
6697

67-
data = asset_data.model_dump()
68-
thing_id = data.pop("thing_id", None)
69-
storage_path = data["storage_path"]
70-
71-
# check to see if an asset entry already exists for
72-
# this storage path and thing_id
73-
existing_asset = check_asset_exists(session, storage_path, thing_id=thing_id)
74-
if existing_asset:
75-
# If an asset already exists, return it
76-
return existing_asset
77-
78-
data["storage_service"] = "gcs"
79-
asset = Asset(**data)
80-
audit_add(user, asset)
81-
82-
if thing_id:
83-
assoc = AssetThingAssociation()
84-
audit_add(user, assoc)
85-
thing = session.get(Thing, thing_id)
86-
assoc.thing = thing
87-
assoc.asset = asset
88-
session.add(assoc)
89-
90-
session.add(asset)
91-
session.commit()
92-
session.refresh(asset)
93-
return asset
98+
try:
99+
data = asset_data.model_dump()
100+
print(data)
101+
thing_id = data.pop("thing_id", None)
102+
print(thing_id)
103+
storage_path = data["storage_path"]
104+
105+
# check to see if an asset entry already exists for
106+
# this storage path and thing_id
107+
existing_asset = check_asset_exists(session, storage_path, thing_id=thing_id)
108+
if existing_asset:
109+
# If an asset already exists, return it
110+
return existing_asset
111+
112+
data["storage_service"] = "gcs"
113+
asset = Asset(**data)
114+
audit_add(user, asset)
115+
116+
if thing_id:
117+
assoc = AssetThingAssociation()
118+
audit_add(user, assoc)
119+
thing = session.get(Thing, thing_id)
120+
assoc.thing = thing
121+
assoc.asset = asset
122+
session.add(assoc)
123+
124+
session.add(asset)
125+
session.commit()
126+
session.refresh(asset)
127+
return asset
128+
except ProgrammingError as e:
129+
database_error_handler(asset_data, e)
94130

95131

96132
# ======= Read =========

schemas/asset.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# ===============================================================================
16-
from pydantic import BaseModel, AwareDatetime
16+
from pydantic import BaseModel
17+
18+
from schemas import ORMBaseModel
1719

1820

1921
class BaseAsset(BaseModel):
@@ -23,26 +25,17 @@ class BaseAsset(BaseModel):
2325
mime_type: str
2426
size: int
2527
uri: str
26-
thing_id: int | None = None
2728

2829

2930
# -------- CREATE ----------
3031
class CreateAsset(BaseAsset):
31-
pass
32+
thing_id: int | None = None
3233

3334

3435
# -------- RESPONSE --------
35-
class AssetResponse(BaseAsset):
36-
id: int
37-
# name: str
38-
# label: str
39-
# storage_service: str
40-
# storage_path: str
41-
# mime_type: str
42-
# size: int
43-
created_at: AwareDatetime
36+
class AssetResponse(ORMBaseModel, BaseAsset):
37+
storage_service: str
4438
storage_service: str
45-
uri: str
4639
signed_url: str | None = None
4740

4841

tests/test_asset.py

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
import pytest
2323

24+
# CLASSES, FIXTURES, AND FUNCTIONS =============================================
25+
2426

2527
class MockBlob:
2628
def upload_from_file(self, *args, **kwargs):
@@ -63,6 +65,9 @@ def override_dependency_fixture():
6365
app.dependency_overrides = {}
6466

6567

68+
# POST & UPLOAD tests ==========================================================
69+
70+
6671
def test_upload_asset():
6772
path = "tests/data/riochama.png"
6873

@@ -78,46 +83,51 @@ def test_upload_asset():
7883

7984

8085
def test_add_asset(thing):
81-
resp = client.post(
82-
"/asset",
83-
json={
84-
"thing_id": thing.id,
85-
"name": "riochama.png",
86-
"storage_service": "mock_service",
87-
"storage_path": "mock/path/to/asset",
88-
"uri": "https://storage.googleapis.com/mock-bucket/mock-asset",
89-
"mime_type": "image/png",
90-
"size": 12345,
91-
},
92-
)
93-
86+
payload = {
87+
"thing_id": thing.id,
88+
"name": "test_asset.png",
89+
"label": "Test Asset",
90+
"uri": "https://storage.googleapis.com/mock-bucket/mock-asset",
91+
"storage_service": "mock_service",
92+
"storage_path": "mock/path/to/asset/test_asset.png",
93+
"mime_type": "image/png",
94+
"size": 12345,
95+
}
96+
resp = client.post("/asset", json=payload)
9497
assert resp.status_code == 201
9598
data = resp.json()
96-
assert data["name"] == "riochama.png"
99+
assert "id" in data
100+
assert "created_at" in data
101+
assert data["name"] == payload["name"]
102+
assert data["label"] == payload["label"]
103+
assert data["uri"] == payload["uri"]
104+
assert data["storage_service"] == "gcs"
105+
assert data["storage_path"] == payload["storage_path"]
106+
assert data["mime_type"] == payload["mime_type"]
107+
assert data["size"] == payload["size"]
97108

98109
cleanup_post_test(Asset, data["id"])
99110

100111

101-
def test_add_asset_with_label(thing):
102-
resp = client.post(
103-
"/asset",
104-
json={
105-
"thing_id": thing.id,
106-
"name": "test_asset.png",
107-
"label": "Test Asset",
108-
"uri": "https://storage.googleapis.com/mock-bucket/mock-asset",
109-
"storage_service": "mock_service",
110-
"storage_path": "mock/path/to/asset/test_asset.png",
111-
"mime_type": "image/png",
112-
"size": 12345,
113-
},
114-
)
115-
assert resp.status_code == 201
112+
def test_add_asset_409_bad_thing_id(thing):
113+
bad_thing_id = 99999
114+
payload = {
115+
"thing_id": bad_thing_id,
116+
"name": "test_asset.png",
117+
"label": "Test Asset",
118+
"uri": "https://storage.googleapis.com/mock-bucket/mock-asset",
119+
"storage_service": "mock_service",
120+
"storage_path": "mock/path/to/asset/test_asset.png",
121+
"mime_type": "image/png",
122+
"size": 12345,
123+
}
124+
resp = client.post("/asset", json=payload)
125+
assert resp.status_code == 409
116126
data = resp.json()
117-
assert data["name"] == "test_asset.png"
118-
assert data["label"] == "Test Asset"
119-
120-
cleanup_post_test(Asset, data["id"])
127+
assert data["detail"][0]["loc"] == ["body", "thing_id"]
128+
assert data["detail"][0]["msg"] == f"Thing with ID {bad_thing_id} not found."
129+
assert data["detail"][0]["type"] == "value_error"
130+
assert data["detail"][0]["input"] == {"thing_id": bad_thing_id}
121131

122132

123133
def test_get_asset(asset):

0 commit comments

Comments
 (0)