From f2330580707b1bf4185fb473e6c1eb2dba1f0f01 Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:31:51 +0100 Subject: [PATCH] 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`. --- backend/app/routers/puck.py | 4 +- backend/app/routers/sample.py | 82 ++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- testfunctions.ipynb | 63 ++++++++++++++------------- 4 files changed, 118 insertions(+), 33 deletions(-) diff --git a/backend/app/routers/puck.py b/backend/app/routers/puck.py index a75e360..4577572 100644 --- a/backend/app/routers/puck.py +++ b/backend/app/routers/puck.py @@ -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, diff --git a/backend/app/routers/sample.py b/backend/app/routers/sample.py index 39db2eb..13ecc6f 100644 --- a/backend/app/routers/sample.py +++ b/backend/app/routers/sample.py @@ -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 + } diff --git a/pyproject.toml b/pyproject.toml index 6460a92..936bf69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/testfunctions.ipynb b/testfunctions.ipynb index 85117be..ba193c5 100644 --- a/testfunctions.ipynb +++ b/testfunctions.ipynb @@ -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": {