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:
parent
f10a5eaec2
commit
f233058070
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"}
|
||||
|
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user