Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/scaup/auth/micro.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ def proposal(
proposalReference: str,
token: HTTPAuthorizationCredentials = Depends(auth_scheme),
):
return _check_perms(proposalReference, "proposal", token.credentials)
proposal_reference = parse_proposal(proposal_reference=proposalReference)
_check_perms(proposalReference, "proposal", token.credentials)
return proposal_reference

@staticmethod
def session(
Expand Down
2 changes: 1 addition & 1 deletion src/scaup/auth/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class GenericPermissions(Protocol):
@staticmethod
def proposal(proposalReference: str):
return proposalReference
return parse_proposal(proposal_reference=proposalReference)

@staticmethod
def session(proposalReference: str, visitNumber: int):
Expand Down
6 changes: 5 additions & 1 deletion src/scaup/crud/proposals.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ def get_shipments(token: str, proposal_reference: ProposalReference, limit: int,
query = select(Shipment).filter(
Shipment.proposalCode == proposal_reference.code,
Shipment.proposalNumber == proposal_reference.number,
Shipment.visitNumber == proposal_reference.visit_number,
)

if proposal_reference.visit_number:
query = query.filter(Shipment.visitNumber == proposal_reference.visit_number)
else:
query = query.order_by(Shipment.visitNumber.desc(), Shipment.creationDate.desc())

shipments: Paged[Shipment] = inner_db.paginate(query, limit, page, slow_count=False, scalar=False)
shipments.items = update_shipment_statuses(shipments.items, token)

Expand Down
2 changes: 2 additions & 0 deletions src/scaup/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
internal,
proposals,
samples,
sessions,
shipments,
top_level_containers,
)
Expand Down Expand Up @@ -72,5 +73,6 @@ async def general_exception_handler(request: Request, exc: Exception):
api.include_router(containers.router)
api.include_router(top_level_containers.router)
api.include_router(internal.router)
api.include_router(sessions.router)

app.mount(os.getenv("MOUNT_POINT", "/api"), api)
44 changes: 44 additions & 0 deletions src/scaup/models/sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from datetime import datetime
from typing import Optional, Set

from pydantic import AliasChoices, Field

from ..utils.models import OrmBaseModel


class SessionOut(OrmBaseModel):
beamLineSetupId: int | None = None
beamCalendarId: int | None = None
startDate: datetime | None = None
endDate: datetime | None = None
beamLineName: str | None = Field(None, max_length=45)
scheduled: int | None = Field(None, lt=10)
nbShifts: int | None = Field(None, lt=1e9)
comments: str | None = Field(None, max_length=2000)
visit_number: int | None = Field(
None,
lt=1e9,
serialization_alias="visitNumber",
validation_alias=AliasChoices("visitNumber", "visit_number"),
)
usedFlag: int | None = Field(
None,
lt=2,
description="Indicates if session has Datacollections or XFE or EnergyScans attached", # noqa: E501
)
lastUpdate: datetime | None = Field(
None,
description="Last update timestamp: by default the end of the session, the last collect", # noqa: E501
)
parentProposal: str | None = None
proposalId: int = Field(..., lt=1e9, description="Proposal ID")
sessionId: int = Field(..., lt=1e9, description="Session ID")
beamLineOperator: Set[str] | None = None
bltimeStamp: datetime
purgedProcessedData: bool
archived: int = Field(
...,
lt=2,
description="The data for the session is archived and no longer available on disk", # noqa: E501
)
collectionGroups: Optional[int] = None
10 changes: 10 additions & 0 deletions src/scaup/routes/proposals.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ def get_shipment_data(
return ExternalRequest.request(token=token.credentials, url=f"/proposals/{proposalReference}/data").json()


@router.get("/{proposalReference}/shipments")
def get_proposal_shipments(
proposalReference: ProposalReference = Depends(Permissions.proposal),
token: HTTPAuthorizationCredentials = Depends(auth_scheme),
page: dict[str, int] = Depends(pagination),
):
"""Get shipments in proposal"""
return crud.get_shipments(token=token.credentials, proposal_reference=proposalReference, **page)


@router.post(
"/{proposalReference}/sessions/{visitNumber}/assign-data-collection-groups",
)
Expand Down
40 changes: 40 additions & 0 deletions src/scaup/routes/sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.security import HTTPAuthorizationCredentials
from lims_utils.logging import app_logger
from lims_utils.models import Paged, pagination

from ..auth import auth_scheme
from ..models.sessions import SessionOut
from ..utils.external import ExternalRequest

router = APIRouter(
tags=["Sessions"],
prefix="/sessions",
)


@router.get("", response_model=Paged[SessionOut])
def get_sessions(
token: HTTPAuthorizationCredentials = Depends(auth_scheme),
page: dict[str, int] = Depends(pagination),
minEndDate: str | None = Query(default=None, description="Minimum session end date"),
):
"""Get sessions a user can view (wrapper for Expeye endpoint)"""
# TODO: replace search once Expeye supports filtering by beamline name
url = f"/sessions?limit={page['limit']}&page={page['page']}&search=m"
if minEndDate is not None:
url += f"&minEndDate={minEndDate}"

expeye_response = ExternalRequest.request(
token=token.credentials,
url=url,
)

if expeye_response.status_code != 200:
app_logger.warning(f"Failed to fetch proposals from Expeye: {expeye_response.text}")
raise HTTPException(
status_code=expeye_response.status_code,
detail="Failed to fetch proposals",
)

return expeye_response.json()
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

@responses.activate
def test_get(client):
"""Should get shipments in proposal"""
"""Should get shipments in session"""
responses.get(
f"{Config.ispyb_api.url}/shipments/63975",
status=200,
Expand All @@ -23,7 +23,7 @@ def test_get(client):

@responses.activate
def test_get_inexistent(client):
"""Should return empty list if no shipments exist in proposal"""
"""Should return empty list if no shipments exist in session"""
resp = client.get("/proposals/cm55555/sessions/1/shipments")

assert resp.status_code == 200
Expand Down
33 changes: 33 additions & 0 deletions tests/proposals/test_get_shipments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import responses

from scaup.utils.config import Config


@responses.activate
def test_get(client):
"""Should get shipments in proposal"""
responses.get(
f"{Config.ispyb_api.url}/shipments/63975",
status=200,
json={"shippingStatus": "opened"},
)

resp = client.get("/proposals/bi23047/shipments")

assert resp.status_code == 200

data = resp.json()

assert len(data["items"]) == 5


@responses.activate
def test_get_inexistent(client):
"""Should return empty list if no shipments exist in proposal"""
resp = client.get("/proposals/cm55555/shipments")

assert resp.status_code == 200

data = resp.json()

assert len(data["items"]) == 0
Empty file added tests/sessions/__init__.py
Empty file.
74 changes: 74 additions & 0 deletions tests/sessions/test_get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import responses

from scaup.utils.config import Config


@responses.activate
def test_get(client):
"""Should get sessions data"""
responses.get(
f"{Config.ispyb_api.url}/sessions",
status=200,
json={
"page": 1,
"total": 99,
"limit": 1,
"items": [
{
"bltimeStamp": "2025-01-01T12:00:00Z",
"visitNumber": 1,
"parentProposal": "cm00001",
"sessionId": 1,
"purgedProcessedData": False,
"proposalId": 1,
"archived": False,
}
],
},
)

resp = client.get("/sessions")

assert resp.status_code == 200

assert resp.json()["total"] == 99


@responses.activate
def test_min_end_date(client):
"""Should include search params in expeye request"""
resp_get = responses.get(
f"{Config.ispyb_api.url}/sessions",
status=200,
json={
"page": 1,
"total": 99,
"limit": 1,
"items": [
{
"bltimeStamp": "2025-01-01T12:00:00Z",
"visitNumber": 1,
"parentProposal": "cm00001",
"sessionId": 1,
"purgedProcessedData": False,
"proposalId": 1,
"archived": False,
}
],
},
)

resp = client.get("/sessions?minEndDate=2025-01-01T00:00:00Z")

assert resp.status_code == 200
assert resp_get.calls[0].request.url.endswith("/sessions?limit=25&page=0&search=m&minEndDate=2025-01-01T00:00:00Z")


@responses.activate
def test_invalid_response(client):
"""Should propagate error if upstream API fails"""
responses.get(f"{Config.ispyb_api.url}/sessions", status=404)

resp = client.get("/sessions")

assert resp.status_code == 404
Loading