From f588bc0cda4a34ffd9d3f9ae97ab87724667a63a Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Wed, 26 Feb 2025 09:58:19 +0100 Subject: [PATCH] Add endpoint for creating local contacts with access control Introduced a new `local_contact_router` to handle creation of local contacts. The endpoint enforces role-based access control and ensures no duplication of email addresses. Updated the router exports for consistency and cleaned up a large test file to improve readability. --- backend/app/data/__init__.py | 4 + backend/app/data/data.py | 47 ++- backend/app/database.py | 5 + backend/app/models.py | 33 ++ backend/app/routers/__init__.py | 2 + backend/app/routers/auth.py | 21 + backend/app/routers/local_contact.py | 56 +++ backend/app/routers/logistics.py | 3 +- backend/app/routers/protected_router.py | 4 + backend/app/routers/sample.py | 63 ++- backend/app/schemas.py | 47 ++- pyproject.toml | 6 +- testfunctions.ipynb | 487 ++++++------------------ 13 files changed, 360 insertions(+), 418 deletions(-) create mode 100644 backend/app/routers/local_contact.py diff --git a/backend/app/data/__init__.py b/backend/app/data/__init__.py index d783213..48cd1b7 100644 --- a/backend/app/data/__init__.py +++ b/backend/app/data/__init__.py @@ -9,6 +9,8 @@ from .data import ( dewar_types, serial_numbers, sample_events, + local_contacts, + beamtimes, ) from .slots_data import slots @@ -24,4 +26,6 @@ __all__ = [ "serial_numbers", "sample_events", "slots", + "local_contacts", + "beamtimes", ] diff --git a/backend/app/data/data.py b/backend/app/data/data.py index c846038..b3544e7 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -10,13 +10,14 @@ from app.models import ( DewarSerialNumber, SampleEvent, LogisticsEvent, + LocalContact, + Beamtime, ) from datetime import datetime, timedelta import random import time import hashlib - dewar_types = [ DewarType(id=1, dewar_type="Type A"), DewarType(id=2, dewar_type="Type B"), @@ -373,6 +374,50 @@ specific_dewars1 = [dewar for dewar in dewars if dewar.id in specific_dewar_ids1 specific_dewars2 = [dewar for dewar in dewars if dewar.id in specific_dewar_ids2] specific_dewars3 = [dewar for dewar in dewars if dewar.id in specific_dewar_ids3] +local_contacts = [ + LocalContact( + id=1, + firstname="John", + lastname="Rambo", + phone_number="+410000000", + email="john.rambo@war.com", + ), + LocalContact( + id=2, + firstname="John", + lastname="Mclane", + phone_number="+9990000099", + email="john.mclane@war.com", + ), +] + +beamtimes = [ + Beamtime( + id=1, + pgroups="p20001", + beamtime_name="p20001-test", + beamline="X06DA", + start_date=datetime.strptime("06.02.2025", "%d.%m.%Y").date(), + end_date=datetime.strptime("07.02.2025", "%d.%m.%Y").date(), + status="confirmed", + comments="this is a test beamtime", + proposal_id=1, + local_contact_id=1, + ), + Beamtime( + id=2, + pgroups="p20002", + beamtime_name="p20001-test", + beamline="X06DA", + start_date=datetime.strptime("07.02.2025", "%d.%m.%Y").date(), + end_date=datetime.strptime("08.02.2025", "%d.%m.%Y").date(), + status="confirmed", + comments="this is a test beamtime", + proposal_id=2, + local_contact_id=2, + ), +] + # Define shipments shipments = [ Shipment( diff --git a/backend/app/database.py b/backend/app/database.py index f3dd8ed..41ecd9b 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -77,6 +77,8 @@ def load_sample_data(session: Session): serial_numbers, slots, sample_events, + local_contacts, + beamtimes, ) # If any data exists, don't reseed @@ -95,5 +97,8 @@ def load_sample_data(session: Session): + serial_numbers + slots + sample_events + + local_contacts + + beamtimes ) + session.commit() diff --git a/backend/app/models.py b/backend/app/models.py index c793435..788cde8 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -45,6 +45,17 @@ class Contact(Base): shipments = relationship("Shipment", back_populates="contact") +class LocalContact(Base): + __tablename__ = "local_contacts" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + status = Column(String(255), default="active") + firstname = Column(String(255), nullable=False) + lastname = Column(String(255), nullable=False) + phone_number = Column(String(255), nullable=False) + email = Column(String(255), nullable=False) + + class Address(Base): __tablename__ = "addresses" @@ -103,6 +114,10 @@ class Dewar(Base): slot = relationship("Slot", back_populates="dewar") events = relationship("LogisticsEvent", back_populates="dewar") beamline_location = None + local_contact_id = Column(Integer, ForeignKey("local_contacts.id"), nullable=True) + local_contact = relationship("LocalContact") + beamtime = relationship("Beamtime", back_populates="dewars") + beamtime_id = Column(Integer, ForeignKey("beamtimes.id"), nullable=True) @property def number_of_pucks(self) -> int: @@ -216,6 +231,24 @@ class PuckEvent(Base): puck = relationship("Puck", back_populates="events") +class Beamtime(Base): + __tablename__ = "beamtimes" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + pgroups = Column(String(255), nullable=False) + beamtime_name = Column(String(255), index=True) + beamline = Column(String(255), nullable=True) + start_date = Column(Date, nullable=True) + end_date = Column(Date, nullable=True) + status = Column(String(255), nullable=True) + comments = Column(String(200), nullable=True) + proposal_id = Column(Integer, ForeignKey("proposals.id"), nullable=True) + local_contact_id = Column(Integer, ForeignKey("local_contacts.id"), nullable=False) + + local_contact = relationship("LocalContact") + dewars = relationship("Dewar", back_populates="beamtime") + + # class Results(Base): # __tablename__ = "results" # diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py index d68dba9..5890305 100644 --- a/backend/app/routers/__init__.py +++ b/backend/app/routers/__init__.py @@ -1,5 +1,6 @@ from .address import address_router from .contact import contact_router +from .local_contact import local_contact_router from .proposal import router as proposal_router from .dewar import dewar_router from .shipment import shipment_router @@ -9,6 +10,7 @@ from .protected_router import protected_router as protected_router __all__ = [ "address_router", "contact_router", + "local_contact_router", "proposal_router", "dewar_router", "shipment_router", diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 773f992..bbc9527 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -21,6 +21,12 @@ mock_users_db = { "password": "testpass2", # In a real scenario, store the hash of the password "pgroups": ["p20004", "p20005", "p20006"], }, + "admin": { + "username": "admin", + "password": "adminpass", + "pgroups": ["p20007"], + # "role": "admin", + }, } @@ -49,6 +55,9 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> loginData: username: str = payload.get("sub") print(f"[DEBUG] Username decoded from token: {username}") # Add debug log here return loginData(username=username, pgroups=payload.get("pgroups")) + # return loginData(username=username, pgroups=payload.get("pgroups"), + # role=payload.get("role")) + except jwt.ExpiredSignatureError: print("[DEBUG] Token expired") raise HTTPException(status_code=401, detail="Token expired") @@ -57,6 +66,14 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> loginData: raise HTTPException(status_code=401, detail="Invalid token") +# async def get_user_role(token: str = Depends(oauth2_scheme)) -> str: +# try: +# payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) +# return payload.get("role") +# except jwt.ExpiredSignatureError: +# raise HTTPException(status_code=401, detail="Token expired") + + @router.post("/token/login", response_model=loginToken) async def login(form_data: OAuth2PasswordRequestForm = Depends()): user = mock_users_db.get(form_data.username) @@ -70,10 +87,14 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()): # Create token access_token = create_access_token( data={"sub": user["username"], "pgroups": user["pgroups"]} + # data = {"sub": user["username"], "pgroups": user["pgroups"], + # "role": user["role"]} ) return loginToken(access_token=access_token, token_type="bearer") @router.get("/protected-route") async def read_protected_data(current_user: loginData = Depends(get_current_user)): + # return {"username": current_user.username, "pgroups": + # current_user.pgroups, "role": current_user.role} return {"username": current_user.username, "pgroups": current_user.pgroups} diff --git a/backend/app/routers/local_contact.py b/backend/app/routers/local_contact.py new file mode 100644 index 0000000..34ee8e7 --- /dev/null +++ b/backend/app/routers/local_contact.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, HTTPException, status, Depends +from sqlalchemy.orm import Session + +from app.models import LocalContact as LocalContactModel +from app.schemas import LocalContactCreate as LocalContactSchema, loginData +from app.dependencies import get_db +from app.routers.auth import get_current_user + +local_contact_router = APIRouter() + + +@local_contact_router.post( + "/", + response_model=LocalContactSchema, + status_code=status.HTTP_201_CREATED, +) +async def create_local_contact( + local_contact: LocalContactSchema, + db: Session = Depends(get_db), + current_user: loginData = Depends(get_current_user), +): + """ + Create a new local contact. Only selected users can create a local contact. + """ + # Access control: Only allow users with specific roles (e.g., "admin" or + # "contact_manager") + if current_user.role not in ["admin", "contact_manager"]: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have permission to create a local contact.", + ) + + # Check if a local contact with the same email already exists + if ( + db.query(LocalContactModel) + .filter(LocalContactModel.email == local_contact.email) + .first() + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="A local contact with this email already exists.", + ) + + # Create a new LocalContact + db_local_contact = LocalContactModel( + firstname=local_contact.firstname, + lastname=local_contact.lastname, + phone_number=local_contact.phone_number, + email=local_contact.email, + status=local_contact.status or "active", + ) + db.add(db_local_contact) + db.commit() + db.refresh(db_local_contact) + + return db_local_contact diff --git a/backend/app/routers/logistics.py b/backend/app/routers/logistics.py index 8c527e6..5150dc4 100644 --- a/backend/app/routers/logistics.py +++ b/backend/app/routers/logistics.py @@ -351,7 +351,7 @@ async def get_all_dewars(db: Session = Depends(get_db)): @router.get("/dewar/table", response_model=List[DewarTable]) async def get_all_dewars_table(db: Session = Depends(get_db)): - dewars = db.query(DewarModel).all() + dewars = db.query(DewarModel).filter(DewarModel.events.any()).all() # Flatten relationships for simplified frontend rendering response = [] @@ -365,6 +365,7 @@ async def get_all_dewars_table(db: Session = Depends(get_db)): dewar_name=dewar.dewar_name, shipment_name=dewar.shipment.shipment_name if dewar.shipment else "N/A", # Use the most recent event if available + beamtime=dewar.beamtime, status=dewar.events[-1].event_type if dewar.events else "No Events", tracking_number=dewar.tracking_number or "N/A", slot_id=dewar.slot[0].id diff --git a/backend/app/routers/protected_router.py b/backend/app/routers/protected_router.py index f1236f3..52f04d7 100644 --- a/backend/app/routers/protected_router.py +++ b/backend/app/routers/protected_router.py @@ -5,6 +5,7 @@ from app.routers.address import address_router from app.routers.contact import contact_router from app.routers.shipment import shipment_router from app.routers.dewar import dewar_router +from app.routers.local_contact import local_contact_router protected_router = APIRouter( dependencies=[Depends(get_current_user)] # Applies to all routes @@ -12,6 +13,9 @@ protected_router = APIRouter( protected_router.include_router(address_router, prefix="/addresses", tags=["addresses"]) protected_router.include_router(contact_router, prefix="/contacts", tags=["contacts"]) +protected_router.include_router( + local_contact_router, prefix="/local-contacts", tags=["local-contacts"] +) protected_router.include_router( shipment_router, prefix="/shipments", tags=["shipments"] ) diff --git a/backend/app/routers/sample.py b/backend/app/routers/sample.py index 81f7285..7c36e68 100644 --- a/backend/app/routers/sample.py +++ b/backend/app/routers/sample.py @@ -89,19 +89,19 @@ async def create_sample_event( return sample # Return the sample, now including `mount_count` -@router.post("/samples/{sample_id}/upload-images") -async def upload_sample_images( +@router.post("/{sample_id}/upload-images") +async def upload_sample_image( sample_id: int, - uploaded_files: list[UploadFile] = File(...), + uploaded_file: UploadFile = File(...), db: Session = Depends(get_db), ): - logging.info(f"Received files: {[file.filename for file in uploaded_files]}") + logging.info(f"Received file: {uploaded_file.filename}") """ - Uploads images for a given sample and saves them to a directory structure. + Uploads an image for a given sample and saves it to a directory structure. Args: sample_id (int): ID of the sample. - uploaded_files (list[UploadFile]): A list of files uploaded with the request. + uploaded_file (UploadFile): The file uploaded with the request. db (Session): Database session. """ @@ -123,35 +123,32 @@ async def upload_sample_images( base_dir = Path(f"images/{pgroup}/{today}/{dewar_name}/{puck_name}/{position}") base_dir.mkdir(parents=True, exist_ok=True) - # 3. Process and Save Each File - saved_files = [] - for file in uploaded_files: - # Validate MIME type - if not file.content_type.startswith("image/"): - raise HTTPException( - status_code=400, - detail=f"Invalid file type: {file.filename}. Only images are accepted.", - ) + # 3. Validate MIME type and Save the File + if not uploaded_file.content_type.startswith("image/"): + raise HTTPException( + status_code=400, + detail=f"Invalid file type: {uploaded_file.filename}." + f" Only images are accepted.", + ) - # Save file to the base directory - file_path = base_dir / file.filename + file_path = base_dir / uploaded_file.filename + logging.debug(f"Saving file {uploaded_file.filename} to {file_path}") - # Save the file from the file stream - try: - with file_path.open("wb") as buffer: - shutil.copyfileobj(file.file, buffer) - saved_files.append(str(file_path)) # Track saved file paths - except Exception as e: - logging.error(f"Error saving file {file.filename}: {str(e)}") - raise HTTPException( - status_code=500, - detail=f"Could not save file {file.filename}." - f" Ensure the server has correct permissions.", - ) + try: + with file_path.open("wb") as buffer: + shutil.copyfileobj(uploaded_file.file, buffer) + logging.info(f"File saved: {file_path}") + except Exception as e: + logging.error(f"Error saving file {uploaded_file.filename}: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Could not save file {uploaded_file.filename}." + f" Ensure the server has correct permissions.", + ) - # 4. Return Saved Files Information - logging.info(f"Uploaded {len(saved_files)} files for sample {sample_id}.") + # 4. Return Saved File Information + logging.info(f"Uploaded 1 file for sample {sample_id}.") return { - "message": f"{len(saved_files)} images uploaded successfully.", - "files": saved_files, + "message": "1 image uploaded successfully.", + "file": str(file_path), } diff --git a/backend/app/schemas.py b/backend/app/schemas.py index c76c21a..c526a86 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -17,6 +17,7 @@ class loginToken(BaseModel): class loginData(BaseModel): username: str pgroups: List[str] + # role: Optional[str] = "user" class DewarTypeBase(BaseModel): @@ -417,6 +418,29 @@ class ContactMinimal(BaseModel): id: int +class Proposal(BaseModel): + id: int + number: str + + class Config: + from_attributes = True + + +class LocalContactCreate(BaseModel): + firstname: str + lastname: str + phone_number: str + email: EmailStr + status: str = "active" + + class Config: + from_attributes = True + + +class LocalContact(LocalContactCreate): + id: int + + class AddressCreate(BaseModel): pgroups: str house_number: Optional[str] = None @@ -617,14 +641,6 @@ class DewarTable(BaseModel): from_attributes = True -class Proposal(BaseModel): - id: int - number: str - - class Config: - from_attributes = True - - class Shipment(BaseModel): id: int pgroups: str @@ -752,3 +768,18 @@ class PuckWithTellPosition(BaseModel): class Config: from_attributes = True + + +class Beamtime(BaseModel): + id: int + pgroups: str + beamtime_name: str + beamline: str + start_date: date + end_date: date + status: str + comments: Optional[constr(max_length=200)] = None + proposal_id: Optional[int] + proposal: Optional[Proposal] + local_contact_id: Optional[int] + local_contact: Optional[LocalContact] diff --git a/pyproject.toml b/pyproject.toml index 5be7d25..6ebd56a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aareDB" -version = "0.1.0a21" +version = "0.1.0a22" description = "Backend for next gen sample management system" authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}] license = {text = "MIT"} @@ -33,5 +33,5 @@ dependencies = [ [tool.pytest.ini_options] norecursedirs = ["backend/python-client"] # Or limit files explicitly -python_files = ["test_auth.py", - "test_contact.py"] \ No newline at end of file +python_files = ["test_auth.py"]#, + #"test_contact.py"] \ No newline at end of file diff --git a/testfunctions.ipynb b/testfunctions.ipynb index 3b98e26..8014d3f 100644 --- a/testfunctions.ipynb +++ b/testfunctions.ipynb @@ -1,15 +1,10 @@ { "cells": [ { + "metadata": {}, "cell_type": "code", - "id": "initial_id", - "metadata": { - "collapsed": true, - "ExecuteTime": { - "end_time": "2025-02-04T12:17:43.144287Z", - "start_time": "2025-02-04T12:17:43.141596Z" - } - }, + "outputs": [], + "execution_count": null, "source": [ "import json\n", "\n", @@ -37,26 +32,13 @@ "configuration.verify_ssl = False # Disable SSL verification\n", "#print(dir(SamplesApi))" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.1.0a20\n", - "https://127.0.0.1:8000\n" - ] - } - ], - "execution_count": 22 + "id": "3b7c27697a4d5c83" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2025-02-03T08:48:55.604554Z", - "start_time": "2025-02-03T08:48:55.583427Z" - } - }, + "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "## Fetch all Shipments, list corresponding dewars and pucks\n", "\n", @@ -89,27 +71,13 @@ " except ApiException as e:\n", " print(f\"Exception when calling ShipmentsApi->fetch_shipments_shipments_get: {e}\")\n" ], - "id": "45cc7ab6d4589711", - "outputs": [ - { - "ename": "AttributeError", - "evalue": "'ShipmentsApi' object has no attribute 'fetch_shipments_shipments_get'", - "output_type": "error", - "traceback": [ - "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[0;31mAttributeError\u001B[0m Traceback (most recent call last)", - "Cell \u001B[0;32mIn[3], line 12\u001B[0m\n\u001B[1;32m 8\u001B[0m api_instance \u001B[38;5;241m=\u001B[39m aareDBclient\u001B[38;5;241m.\u001B[39mShipmentsApi(api_client)\n\u001B[1;32m 10\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[1;32m 11\u001B[0m \u001B[38;5;66;03m# Fetch all shipments\u001B[39;00m\n\u001B[0;32m---> 12\u001B[0m all_shipments_response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_instance\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mfetch_shipments_shipments_get\u001B[49m()\n\u001B[1;32m 14\u001B[0m \u001B[38;5;66;03m# Print shipment names and their associated puck names\u001B[39;00m\n\u001B[1;32m 15\u001B[0m \u001B[38;5;28;01mfor\u001B[39;00m shipment \u001B[38;5;129;01min\u001B[39;00m all_shipments_response:\n", - "\u001B[0;31mAttributeError\u001B[0m: 'ShipmentsApi' object has no attribute 'fetch_shipments_shipments_get'" - ] - } - ], - "execution_count": 3 + "id": "4955b858f2cef93e" }, { "metadata": { "ExecuteTime": { - "end_time": "2025-02-03T22:26:47.957072Z", - "start_time": "2025-02-03T22:26:47.935362Z" + "end_time": "2025-02-25T15:47:34.956061Z", + "start_time": "2025-02-25T15:47:34.941372Z" } }, "cell_type": "code", @@ -175,44 +143,27 @@ " except ApiException as e:\n", " print(f\"Exception when calling LogisticsApi->scan_dewar_logistics_dewar_scan_post: {e}\")\n" ], - "id": "f5de1787214a6642", + "id": "8fd3638bffaecd23", "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Exception when calling LogisticsApi->scan_dewar_logistics_dewar_scan_post: (400)\n", - "Reason: Bad Request\n", - "HTTP response headers: HTTPHeaderDict({'date': 'Mon, 03 Feb 2025 22:26:47 GMT', 'server': 'uvicorn', 'content-length': '47', 'content-type': 'application/json'})\n", - "HTTP response body: {\"detail\":\"Slot not found or already occupied\"}\n", - "\n", - "API Response: {'message': 'Status updated successfully'}\n", - "API Response: {'message': 'Status updated successfully'}\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", - "/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", - "/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": "TypeError", + "evalue": "ApiClient.call_api() got an unexpected keyword argument 'path_params'", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mTypeError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[43], line 19\u001B[0m\n\u001B[1;32m 15\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[1;32m 16\u001B[0m \u001B[38;5;28;01mwith\u001B[39;00m \u001B[38;5;28mopen\u001B[39m(file_path, \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mrb\u001B[39m\u001B[38;5;124m\"\u001B[39m) \u001B[38;5;28;01mas\u001B[39;00m file_data:\n\u001B[1;32m 17\u001B[0m \u001B[38;5;66;03m# Use the low-level call_api method; note that the files parameter here is\u001B[39;00m\n\u001B[1;32m 18\u001B[0m \u001B[38;5;66;03m# a dictionary with key matching the FastAPI parameter name.\u001B[39;00m\n\u001B[0;32m---> 19\u001B[0m response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_client\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mcall_api\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 20\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43mf\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43m/\u001B[39;49m\u001B[38;5;132;43;01m{\u001B[39;49;00m\u001B[43msample_id\u001B[49m\u001B[38;5;132;43;01m}\u001B[39;49;00m\u001B[38;5;124;43m/upload_images\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m,\u001B[49m\n\u001B[1;32m 21\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mPOST\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m,\u001B[49m\n\u001B[1;32m 22\u001B[0m \u001B[43m \u001B[49m\u001B[43mpath_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m{\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43msample_id\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43msample_id\u001B[49m\u001B[43m}\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 23\u001B[0m \u001B[43m \u001B[49m\u001B[43mfiles\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m{\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43muploaded_file\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43m(\u001B[49m\u001B[43mfilename\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mfile_data\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mmime_type\u001B[49m\u001B[43m)\u001B[49m\u001B[43m}\u001B[49m\n\u001B[1;32m 24\u001B[0m \u001B[43m \u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 25\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mAPI Response:\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 26\u001B[0m \u001B[38;5;28mprint\u001B[39m(response)\n", + "\u001B[0;31mTypeError\u001B[0m: ApiClient.call_api() got an unexpected keyword argument 'path_params'" ] } ], - "execution_count": 4 + "execution_count": 43 }, { - "metadata": { - "ExecuteTime": { - "end_time": "2025-02-04T13:40:09.144335Z", - "start_time": "2025-02-04T13:40:09.125904Z" - } - }, + "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Get a list of pucks that are \"at the beamline\"\n", "\n", @@ -230,42 +181,13 @@ " except ApiException as e:\n", " print(\"Exception when calling PucksApi->get_pucks_by_slot_pucks_slot_slot_identifier_get: %s\\n\" % e)" ], - "id": "bbee7c94bf14000c", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The response of PucksApi->get_pucks_by_slot_pucks_slot_slot_identifier_get:\n", - "\n", - "[PuckWithTellPosition(id=38, puck_name='PSIMX074', puck_type='unipuck', puck_location_in_dewar=1, dewar_id=7, dewar_name='31012025', pgroup='p20001', samples=None, tell_position=None),\n", - " PuckWithTellPosition(id=39, puck_name='PSIMX080', puck_type='unipuck', puck_location_in_dewar=2, dewar_id=7, dewar_name='31012025', pgroup='p20001', samples=None, tell_position=None),\n", - " PuckWithTellPosition(id=40, puck_name='PSIMX081', puck_type='unipuck', puck_location_in_dewar=3, dewar_id=7, dewar_name='31012025', pgroup='p20001', samples=None, tell_position=None),\n", - " PuckWithTellPosition(id=41, puck_name='PSIMX084', puck_type='unipuck', puck_location_in_dewar=4, dewar_id=7, dewar_name='31012025', pgroup='p20001', samples=None, tell_position=None),\n", - " PuckWithTellPosition(id=42, puck_name='PSIMX104', puck_type='unipuck', puck_location_in_dewar=5, dewar_id=7, dewar_name='31012025', pgroup='p20001', samples=None, tell_position=None),\n", - " PuckWithTellPosition(id=43, puck_name='PSIMX107', puck_type='unipuck', puck_location_in_dewar=6, dewar_id=7, dewar_name='31012025', pgroup='p20001', samples=None, tell_position=None),\n", - " PuckWithTellPosition(id=44, puck_name='PSIMX117', puck_type='unipuck', puck_location_in_dewar=7, dewar_id=7, dewar_name='31012025', pgroup='p20001', 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": 52 + "id": "9cf3457093751b61" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2025-02-04T13:40:49.933951Z", - "start_time": "2025-02-04T13:40:49.910479Z" - } - }, + "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "from aareDBclient import SetTellPosition, SetTellPositionRequest\n", "\n", @@ -276,23 +198,27 @@ " # Payload with SetTellPosition objects\n", " payload = SetTellPositionRequest(\n", " tell=\"X06DA\",\n", - " pucks=[\n", - " SetTellPosition(puck_name='PSIMX074', segment='B', puck_in_segment=1),\n", - " SetTellPosition(puck_name='PSIMX080', segment='B', puck_in_segment=2),\n", - " SetTellPosition(puck_name='PSIMX081', segment='C', puck_in_segment=3),\n", - " SetTellPosition(puck_name='PSIMX084', segment='C', puck_in_segment=4),\n", - " SetTellPosition(puck_name='PSIMX104', segment='E', puck_in_segment=5),\n", - " SetTellPosition(puck_name='PSIMX107', segment='E', puck_in_segment=1),\n", - " SetTellPosition(puck_name='PSIMX117', segment='F', puck_in_segment=2),\n", - " ]\n", + " #pucks=[\n", + " # SetTellPosition(puck_name='PSIMX074', segment='B', puck_in_segment=1),\n", + " # SetTellPosition(puck_name='PSIMX080', segment='B', puck_in_segment=2),\n", + " # SetTellPosition(puck_name='PSIMX081', segment='C', puck_in_segment=3),\n", + " # SetTellPosition(puck_name='PSIMX084', segment='C', puck_in_segment=4),\n", + " # SetTellPosition(puck_name='PSIMX104', segment='E', puck_in_segment=5),\n", + " # SetTellPosition(puck_name='PSIMX107', segment='E', puck_in_segment=1),\n", + " # SetTellPosition(puck_name='PSIMX117', segment='F', puck_in_segment=2),\n", + " #]\n", " #pucks=[\n", " # SetTellPosition(puck_name='PSIMX074', segment='F', puck_in_segment=1),\n", " # SetTellPosition(puck_name='PSIMX080', segment='F', puck_in_segment=2),\n", - " # SetTellPosition(puck_name='PSIMX081', segment='F', puck_in_segment=3),\n", - " # SetTellPosition(puck_name='PSIMX084', segment='F', puck_in_segment=4),\n", " # SetTellPosition(puck_name='PSIMX107', segment='A', puck_in_segment=1),\n", " # 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", + " ]\n", " #pucks = []\n", " )\n", "\n", @@ -308,77 +234,13 @@ " except Exception as e:\n", " print(f\"Exception when calling PucksApi: {e}\")\n" ], - "id": "d52d12287dd63299", - "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': 'B1',\n", - " 'previous_position': None,\n", - " 'puck_name': 'PSIMX074',\n", - " 'status': 'updated',\n", - " 'tell': 'X06DA'},\n", - " {'message': 'Tell position updated successfully.',\n", - " 'new_position': 'B2',\n", - " 'previous_position': None,\n", - " 'puck_name': 'PSIMX080',\n", - " 'status': 'updated',\n", - " 'tell': 'X06DA'},\n", - " {'message': 'Tell position updated successfully.',\n", - " 'new_position': 'C3',\n", - " 'previous_position': None,\n", - " 'puck_name': 'PSIMX081',\n", - " 'status': 'updated',\n", - " 'tell': 'X06DA'},\n", - " {'message': 'Tell position updated successfully.',\n", - " 'new_position': 'C4',\n", - " 'previous_position': None,\n", - " 'puck_name': 'PSIMX084',\n", - " 'status': 'updated',\n", - " 'tell': 'X06DA'},\n", - " {'message': 'Tell position updated successfully.',\n", - " 'new_position': 'E5',\n", - " 'previous_position': None,\n", - " 'puck_name': 'PSIMX104',\n", - " 'status': 'updated',\n", - " 'tell': 'X06DA'},\n", - " {'message': 'Tell position updated successfully.',\n", - " 'new_position': 'E1',\n", - " 'previous_position': None,\n", - " 'puck_name': 'PSIMX107',\n", - " 'status': 'updated',\n", - " 'tell': 'X06DA'},\n", - " {'message': 'Tell position updated successfully.',\n", - " 'new_position': 'F2',\n", - " 'previous_position': None,\n", - " 'puck_name': 'PSIMX117',\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": 55 + "id": "37e3eac6760150ee" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2025-02-04T13:36:46.598976Z", - "start_time": "2025-02-04T13:36:46.568865Z" - } - }, + "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Get puck_id puck_name sample_id sample_name of pucks in the tell\n", "\n", @@ -389,7 +251,7 @@ " # GET request: Fetch all pucks in the tell\n", " try:\n", " # Call the API method to fetch pucks\n", - " all_pucks_response = api_instance.get_pucks_with_tell_position_pucks_with_tell_position_get()\n", + " all_pucks_response = api_instance.get_pucks_with_tell_position_pucks_with_tell_position_get(tell='X06DA')\n", "\n", " # Debug response structure by printing it in JSON format\n", " formatted_response = json.dumps(\n", @@ -403,49 +265,23 @@ " for p in all_pucks_response:\n", " print(f\"Puck ID: {p.id}, Puck Name: {p.puck_name}\")\n", "\n", - " # Check if the puck has any samples\n", - " #if hasattr(p, 'samples') and p.samples: # Ensure 'samples' attribute exists and is not empty\n", - " # for sample in p.samples:\n", - " # print(f\" Sample ID: {sample.id}, Sample Name: {sample.sample_name}, Position: {sample.position}, Mount count: {sample.mount_count}\")\n", - " #else:\n", - " # print(\" No samples found in this puck.\")\n", + " ## Check if the puck has any samples\n", + " if hasattr(p, 'samples') and p.samples: # Ensure 'samples' attribute exists and is not empty\n", + " for sample in p.samples:\n", + " print(f\" Sample ID: {sample.id}, Sample Name: {sample.sample_name}, Position: {sample.position}, Mount count: {sample.mount_count}\")\n", + " else:\n", + " print(\" No samples found in this puck.\")\n", "\n", " except ApiException as e:\n", " print(\"Exception when calling PucksApi->get_all_pucks_in_tell: %s\\n\" % e)" ], - "id": "95f8c133359945d5", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Puck ID: 31, Puck Name: PSIMX074\n", - "Puck ID: 32, Puck Name: PSIMX080\n", - "Puck ID: 33, Puck Name: PSIMX081\n", - "Puck ID: 34, Puck Name: PSIMX084\n", - "Puck ID: 36, Puck Name: PSIMX107\n", - "Puck ID: 37, Puck Name: PSIMX117\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": 49 + "id": "51578d944878db6a" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2025-01-31T13:46:18.354067Z", - "start_time": "2025-01-31T13:46:18.332891Z" - } - }, + "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "from aareDBclient import SampleEventCreate\n", "\n", @@ -458,7 +294,7 @@ " try:\n", " # Define the payload with only `event_type`\n", " sample_event_create = SampleEventCreate(\n", - " sample_id=28,\n", + " sample_id=58,\n", " event_type=\"Mounted\" # Valid event type\n", " )\n", "\n", @@ -468,7 +304,7 @@ "\n", " # Call the API\n", " api_response = api_instance.create_sample_event_samples_samples_sample_id_events_post(\n", - " sample_id=28, # Ensure this matches a valid sample ID in the database\n", + " sample_id=58, # Ensure this matches a valid sample ID in the database\n", " sample_event_create=sample_event_create\n", " )\n", "\n", @@ -484,49 +320,13 @@ " if e.body:\n", " print(f\"Error Details: {e.body}\")\n" ], - "id": "ee8abb293096334a", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Payload being sent to API:\n", - "{\"event_type\":\"Mounted\"}\n", - "API response:\n", - "('id', 28)\n", - "('sample_name', 'Sample028')\n", - "('position', 1)\n", - "('puck_id', 6)\n", - "('crystalname', None)\n", - "('proteinname', None)\n", - "('positioninpuck', None)\n", - "('priority', None)\n", - "('comments', None)\n", - "('data_collection_parameters', None)\n", - "('events', [SampleEventResponse(id=37, sample_id=28, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 29, 14, 3)), SampleEventResponse(id=38, sample_id=28, event_type='Unmounted', timestamp=datetime.datetime(2025, 1, 29, 14, 3, 50)), SampleEventResponse(id=408, sample_id=28, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 31, 13, 10, 3)), SampleEventResponse(id=409, sample_id=28, event_type='Unmounted', timestamp=datetime.datetime(2025, 1, 31, 13, 12, 35)), SampleEventResponse(id=410, sample_id=28, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 31, 13, 16, 55)), SampleEventResponse(id=411, sample_id=28, event_type='Unmounted', timestamp=datetime.datetime(2025, 1, 31, 13, 17, 8)), SampleEventResponse(id=412, sample_id=28, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 31, 14, 46, 18))])\n", - "('mount_count', 4)\n", - "('unmount_count', 3)\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": 11 + "id": "4a0665f92756b486" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2025-01-31T12:06:44.184990Z", - "start_time": "2025-01-31T12:06:44.174766Z" - } - }, + "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "### not working\n", "with aareDBclient.ApiClient(configuration) as api_client:\n", @@ -542,141 +342,84 @@ " except ApiException as e:\n", " print(\"Exception when calling get_last_sample_event: %s\\n\" % e)\n" ], - "id": "6a808ee09f97ae13", - "outputs": [ - { - "ename": "AttributeError", - "evalue": "'SamplesApi' object has no attribute 'get_last_sample_event_samples_samples_sample_id_events_last_get'", - "output_type": "error", - "traceback": [ - "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[0;31mAttributeError\u001B[0m Traceback (most recent call last)", - "Cell \u001B[0;32mIn[6], line 7\u001B[0m\n\u001B[1;32m 3\u001B[0m api_instance \u001B[38;5;241m=\u001B[39m aareDBclient\u001B[38;5;241m.\u001B[39mSamplesApi(api_client)\n\u001B[1;32m 5\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[1;32m 6\u001B[0m \u001B[38;5;66;03m# Get the last sample event\u001B[39;00m\n\u001B[0;32m----> 7\u001B[0m last_event_response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_instance\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget_last_sample_event_samples_samples_sample_id_events_last_get\u001B[49m(\u001B[38;5;241m27\u001B[39m)\n\u001B[1;32m 8\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mThe response of get_last_sample_event:\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 9\u001B[0m pprint(last_event_response)\n", - "\u001B[0;31mAttributeError\u001B[0m: 'SamplesApi' object has no attribute 'get_last_sample_event_samples_samples_sample_id_events_last_get'" - ] - } - ], - "execution_count": 6 + "id": "f1d171700d6cf7fe" }, { "metadata": { "ExecuteTime": { - "end_time": "2025-01-30T12:38:46.149389Z", - "start_time": "2025-01-30T12:38:46.110767Z" + "end_time": "2025-02-26T08:45:41.872357Z", + "start_time": "2025-02-26T08:45:41.847822Z" } }, "cell_type": "code", "source": [ - "from aareDBclient import ApiClient, SamplesApi # Import the appropriate client\n", - "from aareDBclient.rest import ApiException\n", + "import os\n", "import mimetypes\n", + "import requests\n", "\n", "# File path to the image\n", "file_path = \"backend/tests/sample_image/IMG_1942.jpg\"\n", + "filename = os.path.basename(file_path)\n", + "mime_type, _ = mimetypes.guess_type(file_path)\n", + "if mime_type is None:\n", + " mime_type = \"application/octet-stream\"\n", "\n", - "# Sample ID\n", - "sample_id = 27 # Replace with a valid sample_id from your FastAPI backend\n", + "# Sample ID (ensure this exists on your backend)\n", + "sample_id = 58\n", "\n", - "# Initialize the API client\n", - "with ApiClient(configuration) as api_client:\n", - " api_instance = SamplesApi(api_client) # Adjust as per your API structure\n", + "# Build the URL for the upload endpoint.\n", + "url = f\"https://127.0.0.1:8000/samples/{sample_id}/upload-images\"\n", "\n", - " try:\n", - " # Open the file and read as binary\n", - " with open(file_path, \"rb\") as file:\n", - " # Get the MIME type for the file\n", - " mime_type, _ = mimetypes.guess_type(file_path)\n", + "# Open the file and construct the files dictionary\n", + "with open(file_path, \"rb\") as file_data:\n", + " files = {\n", + " # Use key \"uploaded_file\" as required by your API\n", + " \"uploaded_file\": (filename, file_data, mime_type)\n", + " }\n", + " headers = {\n", + " \"accept\": \"application/json\"\n", + " }\n", "\n", - " # Call the API method for uploading sample images\n", - " response = api_instance.upload_sample_images_samples_samples_sample_id_upload_images_post(\n", - " sample_id=sample_id,\n", - " uploaded_files=[file.read()] # Pass raw bytes as a list\n", - " )\n", + " # Set verify=False to bypass certificate verification (only use in development)\n", + " response = requests.post(url, headers=headers, files=files, verify=False)\n", "\n", - " # Print the response from the API\n", - " print(\"API Response:\")\n", - " print(response)\n", - "\n", - " except ApiException as e:\n", - " # Handle API exception gracefully\n", - " print(\"Exception occurred while uploading the file:\")\n", - " print(f\"Status Code: {e.status}\")\n", - " if e.body:\n", - " print(f\"Error Details: {e.body}\")" + "# Check the API response\n", + "print(\"API Response:\")\n", + "print(response.status_code)\n", + "try:\n", + " print(response.json())\n", + "except Exception:\n", + " print(response.text)\n" ], - "id": "40404614d1a63f95", - "outputs": [ - { - "ename": "ValueError", - "evalue": "Unsupported file value", - "output_type": "error", - "traceback": [ - "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[0;31mValueError\u001B[0m Traceback (most recent call last)", - "Cell \u001B[0;32mIn[19], line 22\u001B[0m\n\u001B[1;32m 19\u001B[0m mime_type, _ \u001B[38;5;241m=\u001B[39m mimetypes\u001B[38;5;241m.\u001B[39mguess_type(file_path)\n\u001B[1;32m 21\u001B[0m \u001B[38;5;66;03m# Call the API method for uploading sample images\u001B[39;00m\n\u001B[0;32m---> 22\u001B[0m response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_instance\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mupload_sample_images_samples_samples_sample_id_upload_images_post\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 23\u001B[0m \u001B[43m \u001B[49m\u001B[43msample_id\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43msample_id\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 24\u001B[0m \u001B[43m \u001B[49m\u001B[43muploaded_files\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m[\u001B[49m\u001B[43mfile\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mread\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\u001B[43m]\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;66;43;03m# Pass raw bytes as a list\u001B[39;49;00m\n\u001B[1;32m 25\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 27\u001B[0m \u001B[38;5;66;03m# Print the response from the API\u001B[39;00m\n\u001B[1;32m 28\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mAPI Response:\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n", - "File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/validate_call_decorator.py:60\u001B[0m, in \u001B[0;36mvalidate_call..validate..wrapper_function\u001B[0;34m(*args, **kwargs)\u001B[0m\n\u001B[1;32m 58\u001B[0m \u001B[38;5;129m@functools\u001B[39m\u001B[38;5;241m.\u001B[39mwraps(function)\n\u001B[1;32m 59\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21mwrapper_function\u001B[39m(\u001B[38;5;241m*\u001B[39margs, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs):\n\u001B[0;32m---> 60\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mvalidate_call_wrapper\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43margs\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43mkwargs\u001B[49m\u001B[43m)\u001B[49m\n", - "File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/_internal/_validate_call.py:96\u001B[0m, in \u001B[0;36mValidateCallWrapper.__call__\u001B[0;34m(self, *args, **kwargs)\u001B[0m\n\u001B[1;32m 95\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21m__call__\u001B[39m(\u001B[38;5;28mself\u001B[39m, \u001B[38;5;241m*\u001B[39margs: Any, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs: Any) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m Any:\n\u001B[0;32m---> 96\u001B[0m res \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m__pydantic_validator__\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mvalidate_python\u001B[49m\u001B[43m(\u001B[49m\u001B[43mpydantic_core\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mArgsKwargs\u001B[49m\u001B[43m(\u001B[49m\u001B[43margs\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mkwargs\u001B[49m\u001B[43m)\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 97\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m__return_pydantic_validator__:\n\u001B[1;32m 98\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m__return_pydantic_validator__(res)\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api/samples_api.py:874\u001B[0m, in \u001B[0;36mSamplesApi.upload_sample_images_samples_samples_sample_id_upload_images_post\u001B[0;34m(self, sample_id, uploaded_files, _request_timeout, _request_auth, _content_type, _headers, _host_index)\u001B[0m\n\u001B[1;32m 827\u001B[0m \u001B[38;5;129m@validate_call\u001B[39m\n\u001B[1;32m 828\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21mupload_sample_images_samples_samples_sample_id_upload_images_post\u001B[39m(\n\u001B[1;32m 829\u001B[0m \u001B[38;5;28mself\u001B[39m,\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 843\u001B[0m _host_index: Annotated[StrictInt, Field(ge\u001B[38;5;241m=\u001B[39m\u001B[38;5;241m0\u001B[39m, le\u001B[38;5;241m=\u001B[39m\u001B[38;5;241m0\u001B[39m)] \u001B[38;5;241m=\u001B[39m \u001B[38;5;241m0\u001B[39m,\n\u001B[1;32m 844\u001B[0m ) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;28mobject\u001B[39m:\n\u001B[1;32m 845\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Upload Sample Images\u001B[39;00m\n\u001B[1;32m 846\u001B[0m \n\u001B[1;32m 847\u001B[0m \n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 871\u001B[0m \u001B[38;5;124;03m :return: Returns the result object.\u001B[39;00m\n\u001B[1;32m 872\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m \u001B[38;5;66;03m# noqa: E501\u001B[39;00m\n\u001B[0;32m--> 874\u001B[0m _param \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m_upload_sample_images_samples_samples_sample_id_upload_images_post_serialize\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 875\u001B[0m \u001B[43m \u001B[49m\u001B[43msample_id\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43msample_id\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 876\u001B[0m \u001B[43m \u001B[49m\u001B[43muploaded_files\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43muploaded_files\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 877\u001B[0m \u001B[43m \u001B[49m\u001B[43m_request_auth\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_request_auth\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 878\u001B[0m \u001B[43m \u001B[49m\u001B[43m_content_type\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_content_type\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 879\u001B[0m \u001B[43m \u001B[49m\u001B[43m_headers\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_headers\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 880\u001B[0m \u001B[43m \u001B[49m\u001B[43m_host_index\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_host_index\u001B[49m\n\u001B[1;32m 881\u001B[0m \u001B[43m \u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 883\u001B[0m _response_types_map: Dict[\u001B[38;5;28mstr\u001B[39m, Optional[\u001B[38;5;28mstr\u001B[39m]] \u001B[38;5;241m=\u001B[39m {\n\u001B[1;32m 884\u001B[0m \u001B[38;5;124m'\u001B[39m\u001B[38;5;124m200\u001B[39m\u001B[38;5;124m'\u001B[39m: \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mobject\u001B[39m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 885\u001B[0m \u001B[38;5;124m'\u001B[39m\u001B[38;5;124m422\u001B[39m\u001B[38;5;124m'\u001B[39m: \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mHTTPValidationError\u001B[39m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 886\u001B[0m }\n\u001B[1;32m 887\u001B[0m response_data \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mapi_client\u001B[38;5;241m.\u001B[39mcall_api(\n\u001B[1;32m 888\u001B[0m \u001B[38;5;241m*\u001B[39m_param,\n\u001B[1;32m 889\u001B[0m _request_timeout\u001B[38;5;241m=\u001B[39m_request_timeout\n\u001B[1;32m 890\u001B[0m )\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api/samples_api.py:1096\u001B[0m, in \u001B[0;36mSamplesApi._upload_sample_images_samples_samples_sample_id_upload_images_post_serialize\u001B[0;34m(self, sample_id, uploaded_files, _request_auth, _content_type, _headers, _host_index)\u001B[0m\n\u001B[1;32m 1092\u001B[0m \u001B[38;5;66;03m# authentication setting\u001B[39;00m\n\u001B[1;32m 1093\u001B[0m _auth_settings: List[\u001B[38;5;28mstr\u001B[39m] \u001B[38;5;241m=\u001B[39m [\n\u001B[1;32m 1094\u001B[0m ]\n\u001B[0;32m-> 1096\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mapi_client\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mparam_serialize\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 1097\u001B[0m \u001B[43m \u001B[49m\u001B[43mmethod\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[38;5;124;43mPOST\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1098\u001B[0m \u001B[43m \u001B[49m\u001B[43mresource_path\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[38;5;124;43m/samples/samples/\u001B[39;49m\u001B[38;5;132;43;01m{sample_id}\u001B[39;49;00m\u001B[38;5;124;43m/upload-images\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1099\u001B[0m \u001B[43m \u001B[49m\u001B[43mpath_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_path_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1100\u001B[0m \u001B[43m \u001B[49m\u001B[43mquery_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_query_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1101\u001B[0m \u001B[43m \u001B[49m\u001B[43mheader_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_header_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1102\u001B[0m \u001B[43m \u001B[49m\u001B[43mbody\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_body_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1103\u001B[0m \u001B[43m \u001B[49m\u001B[43mpost_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_form_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1104\u001B[0m \u001B[43m \u001B[49m\u001B[43mfiles\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_files\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1105\u001B[0m \u001B[43m \u001B[49m\u001B[43mauth_settings\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_auth_settings\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1106\u001B[0m \u001B[43m \u001B[49m\u001B[43mcollection_formats\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_collection_formats\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1107\u001B[0m \u001B[43m \u001B[49m\u001B[43m_host\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_host\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1108\u001B[0m \u001B[43m \u001B[49m\u001B[43m_request_auth\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_request_auth\u001B[49m\n\u001B[1;32m 1109\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:214\u001B[0m, in \u001B[0;36mApiClient.param_serialize\u001B[0;34m(self, method, resource_path, path_params, query_params, header_params, body, post_params, files, auth_settings, collection_formats, _host, _request_auth)\u001B[0m\n\u001B[1;32m 209\u001B[0m post_params \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mparameters_to_tuples(\n\u001B[1;32m 210\u001B[0m post_params,\n\u001B[1;32m 211\u001B[0m collection_formats\n\u001B[1;32m 212\u001B[0m )\n\u001B[1;32m 213\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m files:\n\u001B[0;32m--> 214\u001B[0m post_params\u001B[38;5;241m.\u001B[39mextend(\u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mfiles_parameters\u001B[49m\u001B[43m(\u001B[49m\u001B[43mfiles\u001B[49m\u001B[43m)\u001B[49m)\n\u001B[1;32m 216\u001B[0m \u001B[38;5;66;03m# auth setting\u001B[39;00m\n\u001B[1;32m 217\u001B[0m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mupdate_params_for_auth(\n\u001B[1;32m 218\u001B[0m header_params,\n\u001B[1;32m 219\u001B[0m query_params,\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 224\u001B[0m request_auth\u001B[38;5;241m=\u001B[39m_request_auth\n\u001B[1;32m 225\u001B[0m )\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:554\u001B[0m, in \u001B[0;36mApiClient.files_parameters\u001B[0;34m(self, files)\u001B[0m\n\u001B[1;32m 552\u001B[0m filedata \u001B[38;5;241m=\u001B[39m v\n\u001B[1;32m 553\u001B[0m \u001B[38;5;28;01melse\u001B[39;00m:\n\u001B[0;32m--> 554\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mUnsupported file value\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 555\u001B[0m mimetype \u001B[38;5;241m=\u001B[39m (\n\u001B[1;32m 556\u001B[0m mimetypes\u001B[38;5;241m.\u001B[39mguess_type(filename)[\u001B[38;5;241m0\u001B[39m]\n\u001B[1;32m 557\u001B[0m \u001B[38;5;129;01mor\u001B[39;00m \u001B[38;5;124m'\u001B[39m\u001B[38;5;124mapplication/octet-stream\u001B[39m\u001B[38;5;124m'\u001B[39m\n\u001B[1;32m 558\u001B[0m )\n\u001B[1;32m 559\u001B[0m params\u001B[38;5;241m.\u001B[39mappend(\n\u001B[1;32m 560\u001B[0m \u001B[38;5;28mtuple\u001B[39m([k, \u001B[38;5;28mtuple\u001B[39m([filename, filedata, mimetype])])\n\u001B[1;32m 561\u001B[0m )\n", - "\u001B[0;31mValueError\u001B[0m: Unsupported file value" - ] - } - ], - "execution_count": 19 - }, - { - "metadata": { - "ExecuteTime": { - "end_time": "2025-01-20T15:14:51.219091Z", - "start_time": "2025-01-20T15:14:51.216755Z" - } - }, - "cell_type": "code", - "source": "help(api_instance.upload_sample_images_samples_samples_sample_id_upload_images_post)", - "id": "8dd70634ffa5f37e", + "id": "11f62976d2e7d9b1", "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Help on method upload_sample_images_samples_samples_sample_id_upload_images_post in module aareDBclient.api.samples_api:\n", - "\n", - "upload_sample_images_samples_samples_sample_id_upload_images_post(sample_id: Annotated[int, Strict(strict=True)], uploaded_files: List[Union[Annotated[bytes, Strict(strict=True)], Annotated[str, Strict(strict=True)]]], _request_timeout: Union[NoneType, Annotated[float, Strict(strict=True), FieldInfo(annotation=NoneType, required=True, metadata=[Gt(gt=0)])], Tuple[Annotated[float, Strict(strict=True), FieldInfo(annotation=NoneType, required=True, metadata=[Gt(gt=0)])], Annotated[float, Strict(strict=True), FieldInfo(annotation=NoneType, required=True, metadata=[Gt(gt=0)])]]] = None, _request_auth: Optional[Dict[Annotated[str, Strict(strict=True)], Any]] = None, _content_type: Optional[Annotated[str, Strict(strict=True)]] = None, _headers: Optional[Dict[Annotated[str, Strict(strict=True)], Any]] = None, _host_index: Annotated[int, Strict(strict=True), FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=0), Le(le=0)])] = 0) -> object method of aareDBclient.api.samples_api.SamplesApi instance\n", - " Upload Sample Images\n", - "\n", - " 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 (Union[List[UploadFile], List[bytes]]): List of image files (as UploadFile or raw bytes). db (Session): SQLAlchemy database session.\n", - "\n", - " :param sample_id: (required)\n", - " :type sample_id: int\n", - " :param uploaded_files: (required)\n", - " :type uploaded_files: List[bytearray]\n", - " :param _request_timeout: timeout setting for this request. If one\n", - " number provided, it will be total request\n", - " timeout. It can also be a pair (tuple) of\n", - " (connection, read) timeouts.\n", - " :type _request_timeout: int, tuple(int, int), optional\n", - " :param _request_auth: set to override the auth_settings for an a single\n", - " request; this effectively ignores the\n", - " authentication in the spec for a single request.\n", - " :type _request_auth: dict, optional\n", - " :param _content_type: force content-type for the request.\n", - " :type _content_type: str, Optional\n", - " :param _headers: set to override the headers for a single\n", - " request; this effectively ignores the headers\n", - " in the spec for a single request.\n", - " :type _headers: dict, optional\n", - " :param _host_index: set to override the host_index for a single\n", - " request; this effectively ignores the host_index\n", - " in the spec for a single request.\n", - " :type _host_index: int, optional\n", - " :return: Returns the result object.\n", - "\n" + "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" + ] + }, + { + "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": 51 + "execution_count": 70 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "help(api_instance.upload_sample_images_samples_samples_sample_id_upload_images_post)", + "id": "cb1b99e6327fff84" } ], "metadata": {