Skip to content

Commit 59aaa6f

Browse files
Merge remote-tracking branch 'origin/main' into notebook_entities_v2
* origin/main: Add update endpoints for service admins (#355) Add read endpoints for service admins (#354) Validate types in SimulationExecutionFilter and ElectricalRecordingStimulusFilter (#369)
2 parents 57176a7 + bb0ef66 commit 59aaa6f

File tree

174 files changed

+3709
-1125
lines changed

Some content is hidden

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

174 files changed

+3709
-1125
lines changed

app/db/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ def load_db_model_from_pydantic[I: DeclarativeBase](
7676
created_by_id: uuid.UUID | None,
7777
updated_by_id: uuid.UUID | None,
7878
ignore_attributes: set[str] | None = None,
79+
*,
80+
exclude_defaults: bool = False,
7981
) -> I:
80-
data = json_model.model_dump(by_alias=True)
82+
data = json_model.model_dump(by_alias=True, exclude_defaults=exclude_defaults)
8183

8284
if created_by_id or updated_by_id:
8385
data["created_by_id"] = created_by_id

app/filters/electrical_recording_stimulus.py

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

44
from app.db.model import ElectricalRecordingStimulus
5+
from app.db.types import ElectricalRecordingStimulusShape, ElectricalRecordingStimulusType
56
from app.dependencies.filter import FilterDepends
67
from app.filters.base import CustomFilter
78
from app.filters.common import (
@@ -13,8 +14,8 @@
1314
class ElectricalRecordingStimulusFilter(EntityFilterMixin, NameFilterMixin, CustomFilter):
1415
order_by: list[str] = ["-creation_date"] # noqa: RUF012
1516

16-
shape: str | None = None
17-
injection_type: str | None = None
17+
shape: ElectricalRecordingStimulusShape | None = None
18+
injection_type: ElectricalRecordingStimulusType | None = None
1819

1920
recording_id: uuid.UUID | None = None
2021
recording_id__in: list[uuid.UUID] | None = None

app/filters/simulation_execution.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Annotated
22

33
from app.db.model import SimulationExecution
4+
from app.db.types import SimulationExecutionStatus
45
from app.dependencies.filter import FilterDepends
56
from app.filters.activity import ActivityFilterMixin
67
from app.filters.base import CustomFilter
@@ -9,7 +10,7 @@
910
class SimulationExecutionFilter(CustomFilter, ActivityFilterMixin):
1011
order_by: list[str] = ["-creation_date"] # noqa: RUF012
1112

12-
status: str | None = None
13+
status: SimulationExecutionStatus | None = None
1314

1415
class Constants(CustomFilter.Constants):
1516
model = SimulationExecution

app/queries/common.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from app.schemas.activity import ActivityCreate, ActivityUpdate
3232
from app.schemas.auth import UserContext, UserContextWithProjectId, UserProfile
3333
from app.schemas.types import ListResponse, PaginationResponse
34+
from app.schemas.utils import NOT_SET
3435
from app.utils.uuid import create_uuid
3536

3637

@@ -354,15 +355,17 @@ def router_update_one[T: BaseModel, I: Identifiable](
354355
id_: uuid.UUID,
355356
db: Session,
356357
db_model_class: type[I],
357-
user_context: UserContext,
358+
user_context: UserContext | None,
358359
json_model: BaseModel,
359360
response_schema_class: type[T],
360361
apply_operations: ApplyOperations | None = None,
361362
):
362363
query = (
363364
sa.select(db_model_class).where(db_model_class.id == id_).with_for_update(of=db_model_class)
364365
)
365-
if id_model_class := get_declaring_class(db_model_class, "authorized_project_id"):
366+
if user_context and (
367+
id_model_class := get_declaring_class(db_model_class, "authorized_project_id")
368+
):
366369
query = constrain_to_private_entities(query, user_context, db_model_class=id_model_class)
367370
if apply_operations:
368371
query = apply_operations(query)
@@ -427,13 +430,15 @@ def router_update_activity_one[T: BaseModel, I: Activity](
427430
id_: uuid.UUID,
428431
db: Session,
429432
db_model_class: type[I],
430-
user_context: UserContext | UserContextWithProjectId,
433+
user_context: UserContext | UserContextWithProjectId | None,
431434
json_model: ActivityUpdate,
432435
response_schema_class: type[T],
433436
apply_operations: ApplyOperations | None = None,
434437
) -> T:
435438
query = sa.select(db_model_class).where(db_model_class.id == id_)
436-
if id_model_class := get_declaring_class(db_model_class, "authorized_project_id"):
439+
if user_context and (
440+
id_model_class := get_declaring_class(db_model_class, "authorized_project_id")
441+
):
437442
query = constrain_to_accessible_entities(
438443
query, user_context.project_id, db_model_class=id_model_class
439444
)
@@ -446,21 +451,24 @@ def router_update_activity_one[T: BaseModel, I: Activity](
446451
update_data = json_model.model_dump(
447452
exclude_unset=True,
448453
exclude_none=True,
449-
exclude_defaults=True,
450454
exclude={"used_ids", "generated_ids"},
455+
exclude_defaults=True, # ignore NOT_SET default values
451456
)
452457

453458
for key, value in update_data.items():
454459
setattr(obj, key, value)
455460

456-
if generated_ids := json_model.generated_ids:
461+
# ignore NOT_SET values
462+
generated_ids = json_model.generated_ids if json_model.generated_ids != NOT_SET else []
463+
464+
if generated_ids:
457465
if obj.generated:
458466
raise HTTPException(
459467
status_code=404,
460468
detail="It is forbidden to update generated_ids if they exist.",
461469
)
462470

463-
if (
471+
if user_context and (
464472
unaccessible_entities := db.execute(
465473
select_unauthorized_entities(generated_ids, user_context.project_id)
466474
)

app/repository/asset.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ def get_entity_asset(
1818
entity_type: EntityType,
1919
entity_id: uuid.UUID,
2020
asset_id: uuid.UUID,
21+
*,
22+
include_deleted: bool = False,
2123
) -> Asset:
2224
"""Return a single asset, or raise an error."""
2325
query = (
@@ -26,10 +28,13 @@ def get_entity_asset(
2628
.where(
2729
Asset.entity_id == entity_id,
2830
Asset.id == asset_id,
29-
Asset.status != AssetStatus.DELETED,
3031
Entity.type == entity_type.name,
3132
)
3233
)
34+
# See: https://github.com/openbraininstitute/entitycore/issues/358
35+
if not include_deleted:
36+
query = query.where(Asset.status != AssetStatus.DELETED)
37+
3338
return self.db.execute(query).scalar_one()
3439

3540
def create_entity_asset(self, entity_id: uuid.UUID, asset: AssetCreate) -> Asset:

app/routers/admin.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
from fastapi import APIRouter
44

55
from app.db.utils import RESOURCE_TYPE_TO_CLASS
6+
from app.dependencies.common import PaginationQuery
67
from app.dependencies.db import RepoGroupDep, SessionDep
78
from app.dependencies.s3 import StorageClientFactoryDep
9+
from app.filters.asset import AssetFilterDep
810
from app.queries.common import router_delete_one
911
from app.schemas.asset import (
1012
AssetRead,
1113
)
12-
from app.service import asset as asset_service
14+
from app.schemas.types import ListResponse
15+
from app.service import admin as admin_service, asset as asset_service
1316
from app.utils.routers import EntityRoute, ResourceRoute, entity_route_to_type, route_to_type
1417

1518
router = APIRouter(
@@ -33,6 +36,39 @@ def delete_one(
3336
)
3437

3538

39+
@router.get("/{entity_route}/{entity_id}/assets/{asset_id}")
40+
def get_entity_asset(
41+
repos: RepoGroupDep,
42+
entity_route: EntityRoute,
43+
entity_id: uuid.UUID,
44+
asset_id: uuid.UUID,
45+
) -> AssetRead:
46+
"""Return an asset associated with a specific entity."""
47+
return admin_service.get_entity_asset(
48+
repos=repos,
49+
entity_type=entity_route_to_type(entity_route),
50+
entity_id=entity_id,
51+
asset_id=asset_id,
52+
)
53+
54+
55+
@router.get("/{entity_route}/{entity_id}/assets")
56+
def get_entity_assets(
57+
repos: RepoGroupDep,
58+
entity_route: EntityRoute,
59+
entity_id: uuid.UUID,
60+
pagination_request: PaginationQuery,
61+
filter_model: AssetFilterDep,
62+
) -> ListResponse[AssetRead]:
63+
return admin_service.get_entity_assets(
64+
repos=repos,
65+
entity_route=entity_route,
66+
entity_id=entity_id,
67+
pagination_request=pagination_request,
68+
filter_model=filter_model,
69+
)
70+
71+
3672
@router.delete("/{entity_route}/{entity_id}/assets/{asset_id}")
3773
def delete_entity_asset(
3874
repos: RepoGroupDep,

app/routers/brain_atlas.py

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

33
import app.service.brain_atlas
4+
from app.routers.admin import router as admin_router
5+
6+
ROUTE = "brain-atlas"
47

58
router = APIRouter(
6-
prefix="/brain-atlas",
7-
tags=["brain-atlas"],
9+
prefix=f"/{ROUTE}",
10+
tags=[ROUTE],
811
)
912

1013
read_many = router.get("")(app.service.brain_atlas.read_many)
@@ -13,3 +16,5 @@
1316
read_one_region = router.get("/{atlas_id}/regions/{atlas_region_id}")(
1417
app.service.brain_atlas.read_one_region
1518
)
19+
20+
admin_read_one = admin_router.get(f"/{ROUTE}/{{atlas_id}}")(app.service.brain_atlas.admin_read_one)

app/routers/brain_region.py

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

33
import app.service.brain_region
4+
from app.routers.admin import router as admin_router
5+
6+
ROUTE = "brain-region"
47

58
router = APIRouter(
6-
prefix="/brain-region",
7-
tags=["brain-region"],
9+
prefix=f"/{ROUTE}",
10+
tags=[ROUTE],
811
)
912

1013
read_many = router.get("")(app.service.brain_region.read_many)
1114
read_one = router.get("/{id_}")(app.service.brain_region.read_one)
15+
create_one = router.post("")(app.service.brain_region.create_one)
16+
update_one = router.patch("/{id_}")(app.service.brain_region.update_one)
17+
18+
admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.brain_region.admin_read_one)
19+
admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(
20+
app.service.brain_region.admin_update_one
21+
)

app/routers/calibration.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
from fastapi import APIRouter
22

33
import app.service.calibration
4+
from app.routers.admin import router as admin_router
5+
6+
ROUTE = "calibration"
47

58
router = APIRouter(
6-
prefix="/calibration",
7-
tags=["calibration"],
9+
prefix=f"/{ROUTE}",
10+
tags=[ROUTE],
811
)
912

1013
read_many = router.get("")(app.service.calibration.read_many)
1114
read_one = router.get("/{id_}")(app.service.calibration.read_one)
1215
create_one = router.post("")(app.service.calibration.create_one)
1316
delete_one = router.delete("/{id_}")(app.service.calibration.delete_one)
1417
update_one = router.patch("/{id_}")(app.service.calibration.update_one)
18+
19+
admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.calibration.admin_read_one)
20+
admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.calibration.admin_update_one)

app/routers/cell_composition.py

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

33
import app.service.cell_composition
4+
from app.routers.admin import router as admin_router
5+
6+
ROUTE = "cell-composition"
47

58
router = APIRouter(
6-
prefix="/cell-composition",
7-
tags=["cell-composition"],
9+
prefix=f"/{ROUTE}",
10+
tags=[ROUTE],
811
)
912

1013
read_one = router.get("/{id_}")(app.service.cell_composition.read_one)
1114
read_many = router.get("")(app.service.cell_composition.read_many)
15+
16+
admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.cell_composition.admin_read_one)

0 commit comments

Comments
 (0)