Add Image models and clean up test code structure

Introduced `ImageCreate` and `Image` models to handle image-related data in the backend. Improved the organization and readability of the testing notebook by consolidating and formatting code into distinct sections with markdown cells.
This commit is contained in:
GotthardG 2025-02-26 13:33:23 +01:00
parent f588bc0cda
commit 1606e80f81
5 changed files with 277 additions and 51 deletions

View File

@ -249,6 +249,17 @@ class Beamtime(Base):
dewars = relationship("Dewar", back_populates="beamtime") dewars = relationship("Dewar", back_populates="beamtime")
class Image(Base):
__tablename__ = "images"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
pgroup = Column(String(255), nullable=False)
comment = Column(String(200), nullable=True)
filepath = Column(String(255), nullable=False)
status = Column(String(255), nullable=True)
sample_id = Column(Integer, ForeignKey("samples.id"), nullable=False)
# class Results(Base): # class Results(Base):
# __tablename__ = "results" # __tablename__ = "results"
# #

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pathlib import Path from pathlib import Path
from typing import List from typing import List
@ -9,11 +9,14 @@ from app.schemas import (
Sample as SampleSchema, Sample as SampleSchema,
SampleEventCreate, SampleEventCreate,
Sample, Sample,
Image,
ImageCreate,
) )
from app.models import ( from app.models import (
Puck as PuckModel, Puck as PuckModel,
Sample as SampleModel, Sample as SampleModel,
SampleEvent as SampleEventModel, SampleEvent as SampleEventModel,
Image as ImageModel,
) )
from app.dependencies import get_db from app.dependencies import get_db
import logging import logging
@ -89,29 +92,22 @@ async def create_sample_event(
return sample # Return the sample, now including `mount_count` return sample # Return the sample, now including `mount_count`
@router.post("/{sample_id}/upload-images") @router.post("/{sample_id}/upload-images", response_model=Image)
async def upload_sample_image( async def upload_sample_image(
sample_id: int, sample_id: int,
uploaded_file: UploadFile = File(...), uploaded_file: UploadFile = File(...),
comment: str = Form(None),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
logging.info(f"Received file: {uploaded_file.filename}") logging.info(f"Received file: {uploaded_file.filename}")
""" # Validate Sample
Uploads an image for a given sample and saves it to a directory structure.
Args:
sample_id (int): ID of the sample.
uploaded_file (UploadFile): The file uploaded with the request.
db (Session): Database session.
"""
# 1. Validate Sample
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first() sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
if not sample: if not sample:
raise HTTPException(status_code=404, detail="Sample not found") raise HTTPException(status_code=404, detail="Sample not found")
# 2. Define Directory Structure # Define Directory Structure
pgroup = sample.puck.dewar.pgroups pgroup = sample.puck.dewar.pgroups # adjust to sample or puck pgroups as needed
today = datetime.now().strftime("%Y-%m-%d") today = datetime.now().strftime("%Y-%m-%d")
dewar_name = ( dewar_name = (
sample.puck.dewar.dewar_name sample.puck.dewar.dewar_name
@ -123,7 +119,7 @@ async def upload_sample_image(
base_dir = Path(f"images/{pgroup}/{today}/{dewar_name}/{puck_name}/{position}") base_dir = Path(f"images/{pgroup}/{today}/{dewar_name}/{puck_name}/{position}")
base_dir.mkdir(parents=True, exist_ok=True) base_dir.mkdir(parents=True, exist_ok=True)
# 3. Validate MIME type and Save the File # Validate MIME type and Save the File
if not uploaded_file.content_type.startswith("image/"): if not uploaded_file.content_type.startswith("image/"):
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
@ -146,9 +142,26 @@ async def upload_sample_image(
f" Ensure the server has correct permissions.", f" Ensure the server has correct permissions.",
) )
# 4. Return Saved File Information # Create the payload from the Pydantic schema
logging.info(f"Uploaded 1 file for sample {sample_id}.") image_payload = ImageCreate(
return { pgroup=pgroup,
"message": "1 image uploaded successfully.", comment=comment,
"file": str(file_path), filepath=str(file_path),
} status="active",
sample_id=sample_id,
).dict()
# Convert the payload to your mapped SQLAlchemy model instance.
# Make sure that ImageModel is your mapped model for images.
new_image = ImageModel(**image_payload)
db.add(new_image)
db.commit()
db.refresh(new_image)
logging.info(
f"Uploaded 1 file for sample {sample_id} and"
f" added record {new_image.id} to the database."
)
# Returning the mapped SQLAlchemy object, which will be converted to the
# Pydantic response model.
return new_image

View File

@ -783,3 +783,24 @@ class Beamtime(BaseModel):
proposal: Optional[Proposal] proposal: Optional[Proposal]
local_contact_id: Optional[int] local_contact_id: Optional[int]
local_contact: Optional[LocalContact] local_contact: Optional[LocalContact]
class Config:
from_attributes = True
class ImageCreate(BaseModel):
pgroup: str
sample_id: int
filepath: str
status: str = "active"
comment: Optional[str] = None
class Config:
from_attributes = True
class Image(ImageCreate):
id: int
class Config:
from_attributes = True

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aareDB" name = "aareDB"
version = "0.1.0a22" version = "0.1.0a23"
description = "Backend for next gen sample management system" description = "Backend for next gen sample management system"
authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}] authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}]
license = {text = "MIT"} license = {text = "MIT"}

View File

@ -1,10 +1,13 @@
{ {
"cells": [ "cells": [
{ {
"metadata": {}, "metadata": {
"ExecuteTime": {
"end_time": "2025-02-26T12:02:38.993926Z",
"start_time": "2025-02-26T12:02:38.991283Z"
}
},
"cell_type": "code", "cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [ "source": [
"import json\n", "import json\n",
"\n", "\n",
@ -32,7 +35,18 @@
"configuration.verify_ssl = False # Disable SSL verification\n", "configuration.verify_ssl = False # Disable SSL verification\n",
"#print(dir(SamplesApi))" "#print(dir(SamplesApi))"
], ],
"id": "3b7c27697a4d5c83" "id": "3b7c27697a4d5c83",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.1.0a21\n",
"https://127.0.0.1:8000\n"
]
}
],
"execution_count": 74
}, },
{ {
"metadata": {}, "metadata": {},
@ -160,10 +174,13 @@
"execution_count": 43 "execution_count": 43
}, },
{ {
"metadata": {}, "metadata": {
"ExecuteTime": {
"end_time": "2025-02-26T12:04:05.046079Z",
"start_time": "2025-02-26T12:04:04.954761Z"
}
},
"cell_type": "code", "cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [ "source": [
"# Get a list of pucks that are \"at the beamline\"\n", "# Get a list of pucks that are \"at the beamline\"\n",
"\n", "\n",
@ -181,13 +198,42 @@
" except ApiException as e:\n", " except ApiException as e:\n",
" print(\"Exception when calling PucksApi->get_pucks_by_slot_pucks_slot_slot_identifier_get: %s\\n\" % e)" " print(\"Exception when calling PucksApi->get_pucks_by_slot_pucks_slot_slot_identifier_get: %s\\n\" % e)"
], ],
"id": "9cf3457093751b61" "id": "9cf3457093751b61",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The response of PucksApi->get_pucks_by_slot_pucks_slot_slot_identifier_get:\n",
"\n",
"[PuckWithTellPosition(id=1, puck_name='PUCK-001', puck_type='Unipuck', puck_location_in_dewar=1, dewar_id=1, dewar_name='Dewar One', pgroup='p20001, p20002', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=2, puck_name='PUCK002', puck_type='Unipuck', puck_location_in_dewar=2, dewar_id=1, dewar_name='Dewar One', pgroup='p20001, p20002', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=3, puck_name='PUCK003', puck_type='Unipuck', puck_location_in_dewar=3, dewar_id=1, dewar_name='Dewar One', pgroup='p20001, p20002', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=4, puck_name='PUCK004', puck_type='Unipuck', puck_location_in_dewar=4, dewar_id=1, dewar_name='Dewar One', pgroup='p20001, p20002', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=5, puck_name='PUCK005', puck_type='Unipuck', puck_location_in_dewar=5, dewar_id=1, dewar_name='Dewar One', pgroup='p20001, p20002', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=6, puck_name='PUCK006', puck_type='Unipuck', puck_location_in_dewar=6, dewar_id=1, dewar_name='Dewar One', pgroup='p20001, p20002', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=7, puck_name='PUCK007', puck_type='Unipuck', puck_location_in_dewar=7, dewar_id=1, dewar_name='Dewar One', pgroup='p20001, p20002', samples=None, tell_position=None)]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n"
]
}
],
"execution_count": 77
}, },
{ {
"metadata": {}, "metadata": {
"ExecuteTime": {
"end_time": "2025-02-26T12:04:33.644201Z",
"start_time": "2025-02-26T12:04:33.625894Z"
}
},
"cell_type": "code", "cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [ "source": [
"from aareDBclient import SetTellPosition, SetTellPositionRequest\n", "from aareDBclient import SetTellPosition, SetTellPositionRequest\n",
"\n", "\n",
@ -214,10 +260,10 @@
" # SetTellPosition(puck_name='PSIMX117', segment='A', puck_in_segment=2),\n", " # SetTellPosition(puck_name='PSIMX117', segment='A', puck_in_segment=2),\n",
" #]\n", " #]\n",
" pucks=[\n", " pucks=[\n",
" SetTellPosition(puck_name='PK006', segment='F', puck_in_segment=1),\n", " SetTellPosition(puck_name='PUCK006', segment='F', puck_in_segment=1),\n",
" SetTellPosition(puck_name='PK003', segment='F', puck_in_segment=2),\n", " SetTellPosition(puck_name='PUCK003', segment='F', puck_in_segment=2),\n",
" SetTellPosition(puck_name='PK002', segment='A', puck_in_segment=1),\n", " SetTellPosition(puck_name='PUCK002', segment='A', puck_in_segment=1),\n",
" SetTellPosition(puck_name='PK001', segment='A', puck_in_segment=2),\n", " SetTellPosition(puck_name='PUCK001', segment='A', puck_in_segment=2),\n",
" ]\n", " ]\n",
" #pucks = []\n", " #pucks = []\n",
" )\n", " )\n",
@ -234,13 +280,59 @@
" except Exception as e:\n", " except Exception as e:\n",
" print(f\"Exception when calling PucksApi: {e}\")\n" " print(f\"Exception when calling PucksApi: {e}\")\n"
], ],
"id": "37e3eac6760150ee" "id": "37e3eac6760150ee",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The response of PucksApi->pucks_puck_id_tell_position_put:\n",
"\n",
"[{'message': 'Tell position updated successfully.',\n",
" 'new_position': 'F1',\n",
" 'previous_position': None,\n",
" 'puck_name': 'PUCK006',\n",
" 'status': 'updated',\n",
" 'tell': 'X06DA'},\n",
" {'message': 'Tell position updated successfully.',\n",
" 'new_position': 'F2',\n",
" 'previous_position': None,\n",
" 'puck_name': 'PUCK003',\n",
" 'status': 'updated',\n",
" 'tell': 'X06DA'},\n",
" {'message': 'Tell position updated successfully.',\n",
" 'new_position': 'A1',\n",
" 'previous_position': None,\n",
" 'puck_name': 'PUCK002',\n",
" 'status': 'updated',\n",
" 'tell': 'X06DA'},\n",
" {'message': 'Tell position updated successfully.',\n",
" 'new_position': 'A2',\n",
" 'previous_position': None,\n",
" 'puck_name': 'PUCK001',\n",
" 'status': 'updated',\n",
" 'tell': 'X06DA'}]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n"
]
}
],
"execution_count": 78
}, },
{ {
"metadata": {}, "metadata": {
"ExecuteTime": {
"end_time": "2025-02-26T12:04:41.352140Z",
"start_time": "2025-02-26T12:04:41.331320Z"
}
},
"cell_type": "code", "cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [ "source": [
"# Get puck_id puck_name sample_id sample_name of pucks in the tell\n", "# Get puck_id puck_name sample_id sample_name of pucks in the tell\n",
"\n", "\n",
@ -275,13 +367,69 @@
" except ApiException as e:\n", " except ApiException as e:\n",
" print(\"Exception when calling PucksApi->get_all_pucks_in_tell: %s\\n\" % e)" " print(\"Exception when calling PucksApi->get_all_pucks_in_tell: %s\\n\" % e)"
], ],
"id": "51578d944878db6a" "id": "51578d944878db6a",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Puck ID: 6, Puck Name: PUCK006\n",
" Sample ID: 44, Sample Name: Sample044, Position: 2, Mount count: 1\n",
" Sample ID: 45, Sample Name: Sample045, Position: 3, Mount count: 0\n",
" Sample ID: 46, Sample Name: Sample046, Position: 4, Mount count: 0\n",
" Sample ID: 47, Sample Name: Sample047, Position: 5, Mount count: 1\n",
"Puck ID: 3, Puck Name: PUCK003\n",
" Sample ID: 24, Sample Name: Sample024, Position: 1, Mount count: 0\n",
" Sample ID: 25, Sample Name: Sample025, Position: 5, Mount count: 1\n",
" Sample ID: 26, Sample Name: Sample026, Position: 8, Mount count: 1\n",
" Sample ID: 27, Sample Name: Sample027, Position: 11, Mount count: 1\n",
" Sample ID: 28, Sample Name: Sample028, Position: 12, Mount count: 1\n",
"Puck ID: 2, Puck Name: PUCK002\n",
" Sample ID: 17, Sample Name: Sample017, Position: 4, Mount count: 1\n",
" Sample ID: 18, Sample Name: Sample018, Position: 5, Mount count: 0\n",
" Sample ID: 19, Sample Name: Sample019, Position: 7, Mount count: 1\n",
" Sample ID: 20, Sample Name: Sample020, Position: 10, Mount count: 0\n",
" Sample ID: 21, Sample Name: Sample021, Position: 11, Mount count: 1\n",
" Sample ID: 22, Sample Name: Sample022, Position: 13, Mount count: 0\n",
" Sample ID: 23, Sample Name: Sample023, Position: 16, Mount count: 1\n",
"Puck ID: 1, Puck Name: PUCK-001\n",
" Sample ID: 1, Sample Name: Sample001, Position: 1, Mount count: 1\n",
" Sample ID: 2, Sample Name: Sample002, Position: 2, Mount count: 1\n",
" Sample ID: 3, Sample Name: Sample003, Position: 3, Mount count: 0\n",
" Sample ID: 4, Sample Name: Sample004, Position: 4, Mount count: 0\n",
" Sample ID: 5, Sample Name: Sample005, Position: 5, Mount count: 0\n",
" Sample ID: 6, Sample Name: Sample006, Position: 6, Mount count: 1\n",
" Sample ID: 7, Sample Name: Sample007, Position: 7, Mount count: 0\n",
" Sample ID: 8, Sample Name: Sample008, Position: 8, Mount count: 1\n",
" Sample ID: 9, Sample Name: Sample009, Position: 9, Mount count: 1\n",
" Sample ID: 10, Sample Name: Sample010, Position: 10, Mount count: 1\n",
" Sample ID: 11, Sample Name: Sample011, Position: 11, Mount count: 1\n",
" Sample ID: 12, Sample Name: Sample012, Position: 12, Mount count: 1\n",
" Sample ID: 13, Sample Name: Sample013, Position: 13, Mount count: 0\n",
" Sample ID: 14, Sample Name: Sample014, Position: 14, Mount count: 1\n",
" Sample ID: 15, Sample Name: Sample015, Position: 15, Mount count: 0\n",
" Sample ID: 16, Sample Name: Sample016, Position: 16, Mount count: 0\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n"
]
}
],
"execution_count": 79
}, },
{ {
"metadata": {}, "metadata": {
"ExecuteTime": {
"end_time": "2025-02-26T12:05:03.257159Z",
"start_time": "2025-02-26T12:05:03.232846Z"
}
},
"cell_type": "code", "cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [ "source": [
"from aareDBclient import SampleEventCreate\n", "from aareDBclient import SampleEventCreate\n",
"\n", "\n",
@ -294,7 +442,7 @@
" try:\n", " try:\n",
" # Define the payload with only `event_type`\n", " # Define the payload with only `event_type`\n",
" sample_event_create = SampleEventCreate(\n", " sample_event_create = SampleEventCreate(\n",
" sample_id=58,\n", " sample_id=16,\n",
" event_type=\"Mounted\" # Valid event type\n", " event_type=\"Mounted\" # Valid event type\n",
" )\n", " )\n",
"\n", "\n",
@ -304,7 +452,7 @@
"\n", "\n",
" # Call the API\n", " # Call the API\n",
" api_response = api_instance.create_sample_event_samples_samples_sample_id_events_post(\n", " api_response = api_instance.create_sample_event_samples_samples_sample_id_events_post(\n",
" sample_id=58, # Ensure this matches a valid sample ID in the database\n", " sample_id=16, # Ensure this matches a valid sample ID in the database\n",
" sample_event_create=sample_event_create\n", " sample_event_create=sample_event_create\n",
" )\n", " )\n",
"\n", "\n",
@ -320,7 +468,40 @@
" if e.body:\n", " if e.body:\n",
" print(f\"Error Details: {e.body}\")\n" " print(f\"Error Details: {e.body}\")\n"
], ],
"id": "4a0665f92756b486" "id": "4a0665f92756b486",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Payload being sent to API:\n",
"{\"event_type\":\"Mounted\"}\n",
"API response:\n",
"('id', 16)\n",
"('sample_name', 'Sample016')\n",
"('position', 16)\n",
"('puck_id', 1)\n",
"('crystalname', None)\n",
"('proteinname', None)\n",
"('positioninpuck', None)\n",
"('priority', None)\n",
"('comments', None)\n",
"('data_collection_parameters', None)\n",
"('events', [SampleEventResponse(id=399, sample_id=16, event_type='Mounted', timestamp=datetime.datetime(2025, 2, 26, 13, 5, 3))])\n",
"('mount_count', 1)\n",
"('unmount_count', 0)\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n"
]
}
],
"execution_count": 80
}, },
{ {
"metadata": {}, "metadata": {},
@ -347,8 +528,8 @@
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2025-02-26T08:45:41.872357Z", "end_time": "2025-02-26T12:29:51.615501Z",
"start_time": "2025-02-26T08:45:41.847822Z" "start_time": "2025-02-26T12:29:51.592886Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
@ -399,7 +580,7 @@
"text": [ "text": [
"API Response:\n", "API Response:\n",
"200\n", "200\n",
"{'message': '1 image uploaded successfully.', 'file': 'images/p20001, p20002/2025-02-26/Dewar Two/PK001/14/IMG_1942.jpg'}\n" "{'pgroup': 'p20001, p20002', 'sample_id': 58, 'filepath': 'images/p20001, p20002/2025-02-26/Dewar One/PUCK007/12/IMG_1942.jpg', 'status': 'active', 'comment': None, 'id': 1}\n"
] ]
}, },
{ {
@ -411,7 +592,7 @@
] ]
} }
], ],
"execution_count": 70 "execution_count": 85
}, },
{ {
"metadata": {}, "metadata": {},