Add image upload endpoint and fix puck location handling

Introduced `/samples/{sample_id}/upload-images` API for uploading images tied to samples, validating file types, and saving them in structured directories. Fixed `puck_location_in_dewar` type handling in puck routes. Updated project version in `pyproject.toml`.
This commit is contained in:
GotthardG 2025-01-10 11:31:51 +01:00
parent f10a5eaec2
commit f233058070
4 changed files with 118 additions and 33 deletions

View File

@ -244,7 +244,7 @@ async def get_pucks_with_tell_position(db: Session = Depends(get_db)):
id=int(puck.id),
puck_name=str(puck.puck_name),
puck_type=str(puck.puck_type),
puck_location_in_dewar=str(puck.puck_location_in_dewar)
puck_location_in_dewar=int(puck.puck_location_in_dewar)
if puck.puck_location_in_dewar
else None,
dewar_id=int(puck.dewar_id) if puck.dewar_id else None,
@ -477,7 +477,7 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db))
id=puck.id,
puck_name=puck.puck_name,
puck_type=puck.puck_type,
puck_location_in_dewar=str(puck.puck_location_in_dewar)
puck_location_in_dewar=int(puck.puck_location_in_dewar)
if puck.puck_location_in_dewar
else None,
dewar_id=puck.dewar_id,

View File

@ -1,7 +1,9 @@
from fastapi import APIRouter, HTTPException, Depends
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
from sqlalchemy.orm import Session
from pathlib import Path
from typing import List
from datetime import datetime
import shutil
from app.schemas import (
Puck as PuckSchema,
Sample as SampleSchema,
@ -98,3 +100,81 @@ async def get_last_sample_event(sample_id: int, db: Session = Depends(get_db)):
raise HTTPException(status_code=404, detail="No events found for the sample")
return last_event # Response will automatically use the SampleEventResponse schema
@router.post("/samples/{sample_id}/upload-images")
async def upload_sample_images(
sample_id: int,
uploaded_files: List[UploadFile] = File(...), # Accept multiple files
db: Session = Depends(get_db),
):
"""
Uploads images for a sample and stores them in a directory structure:
images/user/date/dewar_name/puck_name/position/.
Args:
sample_id (int): ID of the sample.
uploaded_files (List[UploadFile]): List of image files to be uploaded.
db (Session): SQLAlchemy database session.
"""
# Fetch sample details from the database
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
if not sample:
raise HTTPException(status_code=404, detail="Sample not found")
# Retrieve associated dewar_name, puck_name and position
puck = sample.puck
if not puck:
raise HTTPException(
status_code=404, detail=f"No puck associated with sample ID {sample_id}"
)
dewar_name = puck.dewar.dewar_name if puck.dewar else None
if not dewar_name:
raise HTTPException(
status_code=404, detail=f"No dewar associated with puck ID {puck.id}"
)
puck_name = puck.puck_name
position = sample.position
# Retrieve username (hardcoded for now—can be fetched dynamically if needed)
username = "e16371"
# Today's date in the format YYYY-MM-DD
today = datetime.now().strftime("%Y-%m-%d")
# Generate the directory path based on the structure
base_dir = (
Path("images") / username / today / dewar_name / puck_name / str(position)
)
# Create directories if they don't exist
base_dir.mkdir(parents=True, exist_ok=True)
# Save each uploaded image to the directory
for file in uploaded_files:
# Validate file content type
if not file.content_type.startswith("image/"):
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {file.filename}. Must be an image.",
)
# Create a file path for storing the uploaded file
file_path = base_dir / file.filename
try:
# Save the file
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error saving file {file.filename}: {str(e)}",
)
return {
"message": f"{len(uploaded_files)} images uploaded successfully.",
"path": str(base_dir), # Return the base directory for reference
}

View File

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

View File

@ -6,8 +6,8 @@
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2025-01-10T09:49:30.517077Z",
"start_time": "2025-01-10T09:49:30.163714Z"
"end_time": "2025-01-10T10:16:49.228369Z",
"start_time": "2025-01-10T10:16:49.224212Z"
}
},
"source": [
@ -21,7 +21,8 @@
"print(aareDBclient.__version__)\n",
"\n",
"configuration = aareDBclient.Configuration(\n",
" host = \"https://mx-aare-test.psi.ch:1492\"\n",
" #host = \"https://mx-aare-test.psi.ch:1492\"\n",
" host = \"https://127.0.0.1:8000\"\n",
")\n",
"\n",
"configuration.verify_ssl = False # Disable SSL verification\n",
@ -32,18 +33,18 @@
"name": "stdout",
"output_type": "stream",
"text": [
"0.1.0a17\n",
"0.1.0a18\n",
"['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_create_sample_event_samples_samples_sample_id_events_post_serialize', '_get_all_pucks_with_samples_and_events_samples_pucks_samples_get_serialize', '_get_last_sample_event_samples_samples_sample_id_events_last_get_serialize', '_get_samples_with_events_samples_puck_id_samples_get_serialize', 'create_sample_event_samples_samples_sample_id_events_post', 'create_sample_event_samples_samples_sample_id_events_post_with_http_info', 'create_sample_event_samples_samples_sample_id_events_post_without_preload_content', 'get_all_pucks_with_samples_and_events_samples_pucks_samples_get', 'get_all_pucks_with_samples_and_events_samples_pucks_samples_get_with_http_info', 'get_all_pucks_with_samples_and_events_samples_pucks_samples_get_without_preload_content', 'get_last_sample_event_samples_samples_sample_id_events_last_get', 'get_last_sample_event_samples_samples_sample_id_events_last_get_with_http_info', 'get_last_sample_event_samples_samples_sample_id_events_last_get_without_preload_content', 'get_samples_with_events_samples_puck_id_samples_get', 'get_samples_with_events_samples_puck_id_samples_get_with_http_info', 'get_samples_with_events_samples_puck_id_samples_get_without_preload_content']\n"
]
}
],
"execution_count": 2
"execution_count": 3
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-01-10T09:49:32.425248Z",
"start_time": "2025-01-10T09:49:32.398398Z"
"end_time": "2025-01-10T10:16:52.480280Z",
"start_time": "2025-01-10T10:16:52.454958Z"
}
},
"cell_type": "code",
@ -70,32 +71,25 @@
"text": [
"The response of PucksApi->get_pucks_by_slot_pucks_slot_slot_identifier_get:\n",
"\n",
"[PuckWithTellPosition(id=1, puck_name='CPS-4093', puck_type='unipuck', puck_location_in_dewar=1, dewar_id=1, dewar_name='Dewar_test', user='e16371', samples=None, tell_position='D1'),\n",
" PuckWithTellPosition(id=2, puck_name='CPS-4178', puck_type='unipuck', puck_location_in_dewar=2, dewar_id=1, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=3, puck_name='PSIMX-122', puck_type='unipuck', puck_location_in_dewar=3, dewar_id=1, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=4, puck_name='E-07', puck_type='unipuck', puck_location_in_dewar=4, dewar_id=1, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=5, puck_name='CPS-6597', puck_type='unipuck', puck_location_in_dewar=5, dewar_id=1, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=6, puck_name='PSIMX-078', puck_type='unipuck', puck_location_in_dewar=6, dewar_id=1, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=7, puck_name='1002', puck_type='unipuck', puck_location_in_dewar=7, dewar_id=1, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=15, puck_name='PSIMX123', puck_type='unipuck', puck_location_in_dewar=1, dewar_id=4, dewar_name='Huang', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=16, puck_name='PSIMX125', puck_type='unipuck', puck_location_in_dewar=2, dewar_id=4, dewar_name='Huang', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=17, puck_name='PSIMX127', puck_type='unipuck', puck_location_in_dewar=3, dewar_id=4, dewar_name='Huang', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=18, puck_name='PSIMX128', puck_type='unipuck', puck_location_in_dewar=4, dewar_id=4, dewar_name='Huang', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=19, puck_name='PSIMX130', puck_type='unipuck', puck_location_in_dewar=5, dewar_id=4, dewar_name='Huang', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=20, puck_name='PSIMX131', puck_type='unipuck', puck_location_in_dewar=6, dewar_id=4, dewar_name='Huang', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=21, puck_name='PSIMX132', puck_type='unipuck', puck_location_in_dewar=7, dewar_id=4, dewar_name='Huang', user='e16371', samples=None, tell_position=None)]\n"
"[PuckWithTellPosition(id=31, puck_name='CPS-4093', puck_type='unipuck', puck_location_in_dewar=1, dewar_id=6, dewar_name='Dewar_test', user='e16371', samples=None, tell_position='C2'),\n",
" PuckWithTellPosition(id=32, puck_name='CPS-4178', puck_type='unipuck', puck_location_in_dewar=2, dewar_id=6, dewar_name='Dewar_test', user='e16371', samples=None, tell_position='C3'),\n",
" PuckWithTellPosition(id=33, puck_name='PSIMX-122', puck_type='unipuck', puck_location_in_dewar=3, dewar_id=6, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=34, puck_name='E-07', puck_type='unipuck', puck_location_in_dewar=4, dewar_id=6, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=35, puck_name='CPS-6597', puck_type='unipuck', puck_location_in_dewar=5, dewar_id=6, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=36, puck_name='PSIMX-078', puck_type='unipuck', puck_location_in_dewar=6, dewar_id=6, dewar_name='Dewar_test', user='e16371', samples=None, tell_position=None),\n",
" PuckWithTellPosition(id=37, puck_name='1002', puck_type='unipuck', puck_location_in_dewar=7, dewar_id=6, dewar_name='Dewar_test', user='e16371', 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 'mx-aare-test.psi.ch'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
"/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": 3
"execution_count": 4
},
{
"metadata": {
@ -160,8 +154,8 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-01-10T09:49:42.543974Z",
"start_time": "2025-01-10T09:49:42.522049Z"
"end_time": "2025-01-10T10:17:48.013415Z",
"start_time": "2025-01-10T10:17:47.983861Z"
}
},
"cell_type": "code",
@ -194,20 +188,31 @@
"output_type": "stream",
"text": [
"The response of PucksApi->get_all_pucks_in_tell:\n",
"\n",
"CPS-4093\n"
"\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 'mx-aare-test.psi.ch'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
"/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"
]
},
{
"ename": "AttributeError",
"evalue": "'PuckWithTellPosition' object has no attribute 'sample_id'",
"output_type": "error",
"traceback": [
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
"\u001B[0;31mAttributeError\u001B[0m Traceback (most recent call last)",
"Cell \u001B[0;32mIn[7], line 17\u001B[0m\n\u001B[1;32m 15\u001B[0m \u001B[38;5;66;03m#print(formatted_response)\u001B[39;00m\n\u001B[1;32m 16\u001B[0m \u001B[38;5;28;01mfor\u001B[39;00m p \u001B[38;5;129;01min\u001B[39;00m all_pucks_response:\n\u001B[0;32m---> 17\u001B[0m \u001B[38;5;28mprint\u001B[39m(p\u001B[38;5;241m.\u001B[39mpuck_name, \u001B[43mp\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43msample_id\u001B[49m)\n\u001B[1;32m 19\u001B[0m \u001B[38;5;28;01mexcept\u001B[39;00m ApiException \u001B[38;5;28;01mas\u001B[39;00m e:\n\u001B[1;32m 20\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mException when calling PucksApi->get_all_pucks_in_tell: \u001B[39m\u001B[38;5;132;01m%s\u001B[39;00m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[38;5;124m\"\u001B[39m \u001B[38;5;241m%\u001B[39m e)\n",
"File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/main.py:856\u001B[0m, in \u001B[0;36mBaseModel.__getattr__\u001B[0;34m(self, item)\u001B[0m\n\u001B[1;32m 853\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28msuper\u001B[39m()\u001B[38;5;241m.\u001B[39m\u001B[38;5;21m__getattribute__\u001B[39m(item) \u001B[38;5;66;03m# Raises AttributeError if appropriate\u001B[39;00m\n\u001B[1;32m 854\u001B[0m \u001B[38;5;28;01melse\u001B[39;00m:\n\u001B[1;32m 855\u001B[0m \u001B[38;5;66;03m# this is the current error\u001B[39;00m\n\u001B[0;32m--> 856\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mAttributeError\u001B[39;00m(\u001B[38;5;124mf\u001B[39m\u001B[38;5;124m'\u001B[39m\u001B[38;5;132;01m{\u001B[39;00m\u001B[38;5;28mtype\u001B[39m(\u001B[38;5;28mself\u001B[39m)\u001B[38;5;241m.\u001B[39m\u001B[38;5;18m__name__\u001B[39m\u001B[38;5;132;01m!r}\u001B[39;00m\u001B[38;5;124m object has no attribute \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mitem\u001B[38;5;132;01m!r}\u001B[39;00m\u001B[38;5;124m'\u001B[39m)\n",
"\u001B[0;31mAttributeError\u001B[0m: 'PuckWithTellPosition' object has no attribute 'sample_id'"
]
}
],
"execution_count": 4
"execution_count": 7
},
{
"metadata": {