Skip to content

Commit 52f8b34

Browse files
authored
Wire in assets to the BrainAtlas and BrainAtlasRead (#207)
1 parent 6856995 commit 52f8b34

File tree

9 files changed

+147
-65
lines changed

9 files changed

+147
-65
lines changed

app/schemas/brain_atlas.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pydantic import BaseModel, ConfigDict
44

5+
from app.schemas.asset import AssetsMixin
56
from app.schemas.base import CreationMixin, IdentifiableMixin, SpeciesRead
67

78

@@ -13,7 +14,7 @@ class BrainAtlasBase(BaseModel):
1314
species: SpeciesRead
1415

1516

16-
class BrainAtlasRead(BrainAtlasBase, CreationMixin, IdentifiableMixin):
17+
class BrainAtlasRead(BrainAtlasBase, CreationMixin, IdentifiableMixin, AssetsMixin):
1718
pass
1819

1920

@@ -27,5 +28,5 @@ class BrainAtlasRegionBase(BaseModel):
2728
brain_region_id: uuid.UUID
2829

2930

30-
class BrainAtlasRegionRead(BrainAtlasRegionBase, CreationMixin, IdentifiableMixin):
31+
class BrainAtlasRegionRead(BrainAtlasRegionBase, CreationMixin, IdentifiableMixin, AssetsMixin):
3132
pass

app/service/brain_atlas.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import uuid
22

3+
from sqlalchemy.orm import selectinload
4+
35
import app.queries.common
46
from app.db.model import BrainAtlas, BrainAtlasRegion
57
from app.dependencies.auth import UserContextDep
@@ -25,7 +27,7 @@ def read_many(
2527
facets=None,
2628
aliases=None,
2729
apply_filter_query_operations=None,
28-
apply_data_query_operations=None,
30+
apply_data_query_operations=lambda select: select.options(selectinload(BrainAtlas.assets)),
2931
pagination_request=pagination_request,
3032
response_schema_class=BrainAtlasRead,
3133
name_to_facet_query_params=None,
@@ -40,7 +42,7 @@ def read_one(user_context: UserContextDep, atlas_id: uuid.UUID, db: SessionDep)
4042
db_model_class=BrainAtlas,
4143
authorized_project_id=user_context.project_id,
4244
response_schema_class=BrainAtlasRead,
43-
apply_operations=None,
45+
apply_operations=lambda select: select.options(selectinload(BrainAtlas.assets)),
4446
)
4547

4648

@@ -62,7 +64,7 @@ def read_many_region(
6264
apply_filter_query_operations=lambda q: q.filter(
6365
BrainAtlasRegion.brain_atlas_id == atlas_id
6466
),
65-
apply_data_query_operations=None,
67+
apply_data_query_operations=lambda s: s.options(selectinload(BrainAtlasRegion.assets)),
6668
pagination_request=pagination_request,
6769
response_schema_class=BrainAtlasRegionRead,
6870
name_to_facet_query_params=None,
@@ -79,5 +81,7 @@ def read_one_region(
7981
db_model_class=BrainAtlasRegion,
8082
authorized_project_id=user_context.project_id,
8183
response_schema_class=BrainAtlasRegionRead,
82-
apply_operations=lambda q: q.filter(BrainAtlasRegion.brain_atlas_id == atlas_id),
84+
apply_operations=lambda select: select.filter(
85+
BrainAtlasRegion.brain_atlas_id == atlas_id
86+
).options(selectinload(BrainAtlasRegion.assets)),
8387
)

app/service/emodel.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ def _load(select: sa.Select[tuple[EModel]]):
4040
selectinload(EModel.contributions).joinedload(Contribution.role),
4141
joinedload(EModel.mtypes),
4242
joinedload(EModel.etypes),
43-
selectinload(EModel.assets),
4443
selectinload(EModel.ion_channel_models).joinedload(IonChannelModel.species),
4544
selectinload(EModel.ion_channel_models).joinedload(IonChannelModel.strain),
4645
selectinload(EModel.ion_channel_models).joinedload(IonChannelModel.brain_region),

tests/routers/test_asset.py

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from unittest.mock import ANY
2-
from uuid import UUID
32

43
import pytest
54

65
from app.db.model import Asset, Entity
76
from app.db.types import AssetLabel, AssetStatus, EntityType
87
from app.errors import ApiErrorCode
9-
from app.routers.asset import EntityRoute
108
from app.schemas.api import ErrorResponse
119
from app.schemas.asset import AssetRead
1210
from app.utils.s3 import build_s3_path
@@ -18,6 +16,8 @@
1816
VIRTUAL_LAB_ID,
1917
add_db,
2018
create_reconstruction_morphology_id,
19+
route,
20+
upload_entity_asset,
2121
)
2222

2323
DIFFERENT_ENTITY_TYPE = "experimental_bouton_density"
@@ -27,28 +27,6 @@
2727
FILE_EXAMPLE_SIZE = 31
2828

2929

30-
def _entity_type_to_route(entity_type: EntityType) -> EntityRoute:
31-
return EntityRoute[entity_type.name]
32-
33-
34-
def _route(entity_type: EntityType) -> str:
35-
return f"/{_entity_type_to_route(entity_type)}"
36-
37-
38-
def _upload_entity_asset(
39-
client, entity_type: EntityType, entity_id: UUID, label: str | None = None
40-
):
41-
with FILE_EXAMPLE_PATH.open("rb") as f:
42-
files = {
43-
# (filename, file (or bytes), content_type, headers)
44-
"file": ("a/b/c.txt", f, "text/plain")
45-
}
46-
data = None
47-
if label:
48-
data = {"label": label}
49-
return client.post(f"{_route(entity_type)}/{entity_id}/assets", files=files, data=data)
50-
51-
5230
def _get_expected_full_path(entity, path):
5331
return build_s3_path(
5432
vlab_id=VIRTUAL_LAB_ID,
@@ -73,6 +51,17 @@ def entity(client, species_id, strain_id, brain_region_id) -> Entity:
7351
return Entity(id=entity_id, type=entity_type)
7452

7553

54+
def _upload_entity_asset(client, entity_type, entity_id, label=None):
55+
with FILE_EXAMPLE_PATH.open("rb") as f:
56+
files = {
57+
# (filename, file (or bytes), content_type, headers)
58+
"file": ("a/b/c.txt", f, "text/plain")
59+
}
60+
return upload_entity_asset(
61+
client=client, entity_type=entity_type, entity_id=entity_id, files=files, label=label
62+
)
63+
64+
7665
@pytest.fixture
7766
def asset(client, entity) -> AssetRead:
7867
response = _upload_entity_asset(client, entity_type=entity.type, entity_id=entity.id)
@@ -178,7 +167,7 @@ def test_upload_entity_asset__label(monkeypatch, client, entity):
178167

179168

180169
def test_get_entity_asset(client, entity, asset):
181-
response = client.get(f"{_route(entity.type)}/{entity.id}/assets/{asset.id}")
170+
response = client.get(f"{route(entity.type)}/{entity.id}/assets/{asset.id}")
182171

183172
assert response.status_code == 200, f"Failed to get asset: {response.text}"
184173
data = response.json()
@@ -197,20 +186,20 @@ def test_get_entity_asset(client, entity, asset):
197186
}
198187

199188
# try to get an asset with non-existent entity id
200-
response = client.get(f"{_route(entity.type)}/{MISSING_ID}/assets/{asset.id}")
189+
response = client.get(f"{route(entity.type)}/{MISSING_ID}/assets/{asset.id}")
201190
assert response.status_code == 404, f"Unexpected result: {response.text}"
202191
error = ErrorResponse.model_validate(response.json())
203192
assert error.error_code == ApiErrorCode.ENTITY_NOT_FOUND
204193

205194
# try to get an asset with non-existent asset id
206-
response = client.get(f"{_route(entity.type)}/{entity.id}/assets/{MISSING_ID}")
195+
response = client.get(f"{route(entity.type)}/{entity.id}/assets/{MISSING_ID}")
207196
assert response.status_code == 404, f"Unexpected result: {response.text}"
208197
error = ErrorResponse.model_validate(response.json())
209198
assert error.error_code == ApiErrorCode.ASSET_NOT_FOUND
210199

211200

212201
def test_get_entity_assets(client, entity, asset):
213-
response = client.get(f"{_route(entity.type)}/{entity.id}/assets")
202+
response = client.get(f"{route(entity.type)}/{entity.id}/assets")
214203

215204
assert response.status_code == 200, f"Failed to get asset: {response.text}"
216205
data = response.json()["data"]
@@ -231,15 +220,15 @@ def test_get_entity_assets(client, entity, asset):
231220
]
232221

233222
# try to get assets with non-existent entity id
234-
response = client.get(f"{_route(entity.type)}/{MISSING_ID}/assets")
223+
response = client.get(f"{route(entity.type)}/{MISSING_ID}/assets")
235224
assert response.status_code == 404, f"Unexpected result: {response.text}"
236225
error = ErrorResponse.model_validate(response.json())
237226
assert error.error_code == ApiErrorCode.ENTITY_NOT_FOUND
238227

239228

240229
def test_download_entity_asset(client, entity, asset):
241230
response = client.get(
242-
f"{_route(entity.type)}/{entity.id}/assets/{asset.id}/download",
231+
f"{route(entity.type)}/{entity.id}/assets/{asset.id}/download",
243232
follow_redirects=False,
244233
)
245234

@@ -250,20 +239,20 @@ def test_download_entity_asset(client, entity, asset):
250239
assert expected_params.issubset(response.next_request.url.params)
251240

252241
# try to download an asset with non-existent entity id
253-
response = client.get(f"{_route(entity.type)}/{MISSING_ID}/assets/{asset.id}/download")
242+
response = client.get(f"{route(entity.type)}/{MISSING_ID}/assets/{asset.id}/download")
254243
assert response.status_code == 404, f"Unexpected result: {response.text}"
255244
error = ErrorResponse.model_validate(response.json())
256245
assert error.error_code == ApiErrorCode.ENTITY_NOT_FOUND
257246

258247
# try to download an asset with non-existent asset id
259-
response = client.get(f"{_route(entity.type)}/{entity.id}/assets/{MISSING_ID}/download")
248+
response = client.get(f"{route(entity.type)}/{entity.id}/assets/{MISSING_ID}/download")
260249
assert response.status_code == 404, f"Unexpected result: {response.text}"
261250
error = ErrorResponse.model_validate(response.json())
262251
assert error.error_code == ApiErrorCode.ASSET_NOT_FOUND
263252

264253
# when downloading a single file asset_path should not be passed as a parameter
265254
response = client.get(
266-
f"{_route(entity.type)}/{entity.id}/assets/{asset.id}/download",
255+
f"{route(entity.type)}/{entity.id}/assets/{asset.id}/download",
267256
params={"asset_path": "foo"},
268257
follow_redirects=False,
269258
)
@@ -273,21 +262,21 @@ def test_download_entity_asset(client, entity, asset):
273262

274263

275264
def test_delete_entity_asset(client, entity, asset):
276-
response = client.delete(f"{_route(entity.type)}/{entity.id}/assets/{asset.id}")
265+
response = client.delete(f"{route(entity.type)}/{entity.id}/assets/{asset.id}")
277266
assert response.status_code == 200, f"Failed to delete asset: {response.text}"
278267
data = response.json()
279268
assert data == asset.model_copy(update={"status": AssetStatus.DELETED}).model_dump(mode="json")
280269

281270
# try to delete again the same asset
282-
response = client.delete(f"{_route(entity.type)}/{entity.id}/assets/{asset.id}")
271+
response = client.delete(f"{route(entity.type)}/{entity.id}/assets/{asset.id}")
283272
assert response.status_code == 404, f"Unexpected result: {response.text}"
284273

285274
# try to delete an asset with non-existent entity id
286-
response = client.delete(f"{_route(entity.type)}/{MISSING_ID}/assets/{asset.id}")
275+
response = client.delete(f"{route(entity.type)}/{MISSING_ID}/assets/{asset.id}")
287276
assert response.status_code == 404, f"Unexpected result: {response.text}"
288277

289278
# try to delete an asset with non-existent asset id
290-
response = client.delete(f"{_route(entity.type)}/{entity.id}/assets/{MISSING_ID}")
279+
response = client.delete(f"{route(entity.type)}/{entity.id}/assets/{MISSING_ID}")
291280
assert response.status_code == 404, f"Unexpected result: {response.text}"
292281

293282

@@ -297,7 +286,7 @@ def test_upload_delete_upload_entity_asset(client, entity):
297286
data = response.json()
298287
asset0 = AssetRead.model_validate(data)
299288

300-
response = client.delete(f"{_route(entity.type)}/{entity.id}/assets/{asset0.id}")
289+
response = client.delete(f"{route(entity.type)}/{entity.id}/assets/{asset0.id}")
301290
assert response.status_code == 200, f"Failed to delete asset: {response.text}"
302291

303292
# upload the asset with the same path
@@ -307,7 +296,7 @@ def test_upload_delete_upload_entity_asset(client, entity):
307296
asset1 = AssetRead.model_validate(data)
308297

309298
# test that the deleted assets are filtered out
310-
response = client.get(f"{_route(entity.type)}/{entity.id}/assets")
299+
response = client.get(f"{route(entity.type)}/{entity.id}/assets")
311300

312301
assert response.status_code == 200, f"Failed to get assest: {response.text}"
313302
data = response.json()["data"]
@@ -320,15 +309,15 @@ def test_upload_delete_upload_entity_asset(client, entity):
320309

321310
def test_download_directory_file(client, entity, asset_directory):
322311
response = client.get(
323-
url=f"{_route(entity.type)}/{entity.id}/assets/{asset_directory.id}/download",
312+
url=f"{route(entity.type)}/{entity.id}/assets/{asset_directory.id}/download",
324313
params={"asset_path": "file1.txt"},
325314
follow_redirects=False,
326315
)
327316
assert response.status_code == 307, f"Failed to download directory file: {response.text}"
328317

329318
# asset_path is mandatory if the asset is a direcotory
330319
response = client.get(
331-
url=f"{_route(entity.type)}/{entity.id}/assets/{asset_directory.id}/download",
320+
url=f"{route(entity.type)}/{entity.id}/assets/{asset_directory.id}/download",
332321
follow_redirects=False,
333322
)
334323
assert response.status_code == 409, (

0 commit comments

Comments
 (0)