From 1606e80f81e8884f505d525cc866dc75a203f96d Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:33:23 +0100 Subject: [PATCH] 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. --- backend/app/models.py | 11 ++ backend/app/routers/sample.py | 53 +++++--- backend/app/schemas.py | 21 +++ pyproject.toml | 2 +- testfunctions.ipynb | 241 +++++++++++++++++++++++++++++----- 5 files changed, 277 insertions(+), 51 deletions(-) diff --git a/backend/app/models.py b/backend/app/models.py index 788cde8..a69d99a 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -249,6 +249,17 @@ class Beamtime(Base): 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): # __tablename__ = "results" # diff --git a/backend/app/routers/sample.py b/backend/app/routers/sample.py index 7c36e68..201df60 100644 --- a/backend/app/routers/sample.py +++ b/backend/app/routers/sample.py @@ -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 pathlib import Path from typing import List @@ -9,11 +9,14 @@ from app.schemas import ( Sample as SampleSchema, SampleEventCreate, Sample, + Image, + ImageCreate, ) from app.models import ( Puck as PuckModel, Sample as SampleModel, SampleEvent as SampleEventModel, + Image as ImageModel, ) from app.dependencies import get_db import logging @@ -89,29 +92,22 @@ async def create_sample_event( 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( sample_id: int, uploaded_file: UploadFile = File(...), + comment: str = Form(None), db: Session = Depends(get_db), ): logging.info(f"Received file: {uploaded_file.filename}") - """ - 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 + # Validate Sample sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first() if not sample: raise HTTPException(status_code=404, detail="Sample not found") - # 2. Define Directory Structure - pgroup = sample.puck.dewar.pgroups + # Define Directory Structure + pgroup = sample.puck.dewar.pgroups # adjust to sample or puck pgroups as needed today = datetime.now().strftime("%Y-%m-%d") 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.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/"): raise HTTPException( status_code=400, @@ -146,9 +142,26 @@ async def upload_sample_image( f" Ensure the server has correct permissions.", ) - # 4. Return Saved File Information - logging.info(f"Uploaded 1 file for sample {sample_id}.") - return { - "message": "1 image uploaded successfully.", - "file": str(file_path), - } + # Create the payload from the Pydantic schema + image_payload = ImageCreate( + pgroup=pgroup, + comment=comment, + 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 diff --git a/backend/app/schemas.py b/backend/app/schemas.py index c526a86..b143415 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -783,3 +783,24 @@ class Beamtime(BaseModel): proposal: Optional[Proposal] local_contact_id: Optional[int] 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 diff --git a/pyproject.toml b/pyproject.toml index 6ebd56a..3e87e4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aareDB" -version = "0.1.0a22" +version = "0.1.0a23" 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 8014d3f..7f9ccf0 100644 --- a/testfunctions.ipynb +++ b/testfunctions.ipynb @@ -1,10 +1,13 @@ { "cells": [ { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-26T12:02:38.993926Z", + "start_time": "2025-02-26T12:02:38.991283Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "import json\n", "\n", @@ -32,7 +35,18 @@ "configuration.verify_ssl = False # Disable SSL verification\n", "#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": {}, @@ -160,10 +174,13 @@ "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", - "outputs": [], - "execution_count": null, "source": [ "# Get a list of pucks that are \"at the beamline\"\n", "\n", @@ -181,13 +198,42 @@ " except ApiException as e:\n", " 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", - "outputs": [], - "execution_count": null, "source": [ "from aareDBclient import SetTellPosition, SetTellPositionRequest\n", "\n", @@ -214,10 +260,10 @@ " # SetTellPosition(puck_name='PSIMX117', segment='A', puck_in_segment=2),\n", " #]\n", " pucks=[\n", - " SetTellPosition(puck_name='PK006', segment='F', puck_in_segment=1),\n", - " SetTellPosition(puck_name='PK003', segment='F', puck_in_segment=2),\n", - " SetTellPosition(puck_name='PK002', segment='A', puck_in_segment=1),\n", - " SetTellPosition(puck_name='PK001', segment='A', puck_in_segment=2),\n", + " SetTellPosition(puck_name='PUCK006', segment='F', puck_in_segment=1),\n", + " SetTellPosition(puck_name='PUCK003', segment='F', puck_in_segment=2),\n", + " SetTellPosition(puck_name='PUCK002', segment='A', puck_in_segment=1),\n", + " SetTellPosition(puck_name='PUCK001', segment='A', puck_in_segment=2),\n", " ]\n", " #pucks = []\n", " )\n", @@ -234,13 +280,59 @@ " except Exception as 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", - "outputs": [], - "execution_count": null, "source": [ "# Get puck_id puck_name sample_id sample_name of pucks in the tell\n", "\n", @@ -275,13 +367,69 @@ " except ApiException as e:\n", " 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", - "outputs": [], - "execution_count": null, "source": [ "from aareDBclient import SampleEventCreate\n", "\n", @@ -294,7 +442,7 @@ " try:\n", " # Define the payload with only `event_type`\n", " sample_event_create = SampleEventCreate(\n", - " sample_id=58,\n", + " sample_id=16,\n", " event_type=\"Mounted\" # Valid event type\n", " )\n", "\n", @@ -304,7 +452,7 @@ "\n", " # Call the API\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", " )\n", "\n", @@ -320,7 +468,40 @@ " if 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": {}, @@ -347,8 +528,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-02-26T08:45:41.872357Z", - "start_time": "2025-02-26T08:45:41.847822Z" + "end_time": "2025-02-26T12:29:51.615501Z", + "start_time": "2025-02-26T12:29:51.592886Z" } }, "cell_type": "code", @@ -399,7 +580,7 @@ "text": [ "API Response:\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": {},