diff --git a/backend/app/data/data.py b/backend/app/data/data.py index ad5914e..55887e8 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -110,36 +110,56 @@ contacts = [ return_addresses = [ Address( id=1, - street="123 Hobbiton St", - city="Shire", + pgroups="p20000, p20002", + status="active", + house_number="123", + street="Hobbiton St", + city="Hobbitbourg", + state="Shire", zipcode="12345", country="Middle Earth", ), Address( id=2, - street="456 Rohan Rd", + pgroups="p20000, p20001", + status="active", + house_number="456", + street="Rohan Rd", city="Edoras", + state="Rohan", zipcode="67890", country="Middle Earth", ), Address( id=3, - street="789 Greenwood Dr", + pgroups="p20001, p20002", + status="active", + house_number="789", + street="Greenwood Dr", city="Mirkwood", + state="Greenwood", zipcode="13579", country="Middle Earth", ), Address( id=4, - street="321 Gondor Ave", + pgroups="p20001, p20002, p20003", + status="active", + house_number="321", + street="Gondor Ave", city="Minas Tirith", + state="Gondor", zipcode="24680", country="Middle Earth", ), Address( id=5, - street="654 Falgorn Pass", + pgroups="p20004, p20005", + status="active", + house_number="654", + street="Falgorn Pass", city="Rivendell", + state="Rivendell", zipcode="11223", country="Middle Earth", ), @@ -234,11 +254,11 @@ dewars = [ # Define proposals proposals = [ - Proposal(id=1, number="p20000"), - Proposal(id=2, number="p20001"), - Proposal(id=3, number="p20002"), - Proposal(id=4, number="p20003"), - Proposal(id=5, number="p20004"), + Proposal(id=1, number="202400125"), + Proposal(id=2, number="202400235"), + Proposal(id=3, number="202400237"), + Proposal(id=4, number="202400336"), + Proposal(id=5, number="202400255"), ] # Define shipment specific dewars diff --git a/backend/app/models.py b/backend/app/models.py index 5d3c467..015a3ed 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -46,10 +46,14 @@ class Address(Base): __tablename__ = "addresses" id = Column(Integer, primary_key=True, index=True, autoincrement=True) - street = Column(String(255)) - city = Column(String(255)) - zipcode = Column(String(255)) - country = Column(String(255)) + status = Column(String(255), default="active") + pgroups = Column(String(255), nullable=False) + street = Column(String(255), nullable=False) + house_number = Column(String(255), nullable=True) + city = Column(String(255), nullable=False) + state = Column(String(255), nullable=True) + zipcode = Column(String(255), nullable=False) + country = Column(String(255), nullable=False) shipments = relationship("Shipment", back_populates="return_address") diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py index c5e8b92..06abfac 100644 --- a/backend/app/routers/__init__.py +++ b/backend/app/routers/__init__.py @@ -1,9 +1,10 @@ -from .address import router as address_router +from .address import protected_router as address_router from .contact import router as contact_router from .proposal import router as proposal_router from .dewar import router as dewar_router from .shipment import router as shipment_router from .auth import router as auth_router +from .protected_router import protected_router as protected_router __all__ = [ "address_router", @@ -12,4 +13,5 @@ __all__ = [ "dewar_router", "shipment_router", "auth_router", + "protected_router", ] diff --git a/backend/app/routers/address.py b/backend/app/routers/address.py index 478d967..303902f 100644 --- a/backend/app/routers/address.py +++ b/backend/app/routers/address.py @@ -1,20 +1,63 @@ -from fastapi import APIRouter, HTTPException, status, Depends +from fastapi import Depends, HTTPException, status, Query from sqlalchemy.orm import Session +from sqlalchemy import or_ from typing import List -from app.schemas import Address as AddressSchema, AddressCreate, AddressUpdate + +from app.routers.auth import get_current_user +from app.schemas import ( + Address as AddressSchema, + AddressCreate, + AddressUpdate, + loginData, +) from app.models import Address as AddressModel from app.dependencies import get_db - -router = APIRouter() +from app.routers.protected_router import protected_router -@router.get("/", response_model=List[AddressSchema]) -async def get_return_addresses(db: Session = Depends(get_db)): - return db.query(AddressModel).all() +@protected_router.get("/", response_model=List[AddressSchema]) +async def get_return_addresses( + active_pgroup: str = Query(...), + db: Session = Depends(get_db), + current_user: loginData = Depends(get_current_user), +): + if active_pgroup not in current_user.pgroups: + raise HTTPException(status_code=400, detail="Invalid pgroup provided.") + + # Return only active addresses + user_addresses = ( + db.query(AddressModel) + .filter( + AddressModel.pgroups.like(f"%{active_pgroup}%"), + AddressModel.status == "active", + ) + .all() + ) + return user_addresses -@router.post("/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED) +@protected_router.get("/all", response_model=List[AddressSchema]) +async def get_all_addresses( + db: Session = Depends(get_db), + current_user: loginData = Depends(get_current_user), +): + # Fetch all active addresses associated with the user's pgroups + user_pgroups = current_user.pgroups + filters = [AddressModel.pgroups.like(f"%{pgroup}%") for pgroup in user_pgroups] + user_addresses = ( + db.query(AddressModel) + .filter(AddressModel.status == "active", or_(*filters)) + .all() + ) + return user_addresses + + +@protected_router.post( + "/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED +) async def create_return_address(address: AddressCreate, db: Session = Depends(get_db)): + print("Payload received by backend:", address.dict()) # Log incoming payload + if db.query(AddressModel).filter(AddressModel.city == address.city).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -22,10 +65,14 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge ) db_address = AddressModel( + pgroups=address.pgroups, + house_number=address.house_number, street=address.street, city=address.city, + state=address.state, zipcode=address.zipcode, country=address.country, + status="active", ) db.add(db_address) @@ -34,29 +81,74 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge return db_address -@router.put("/{address_id}", response_model=AddressSchema) +@protected_router.put("/{address_id}", response_model=AddressSchema) async def update_return_address( address_id: int, address: AddressUpdate, db: Session = Depends(get_db) ): + # Retrieve the existing address db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first() if not db_address: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Address not found." ) - for key, value in address.dict(exclude_unset=True).items(): - setattr(db_address, key, value) + + # Normalize existing and new pgroups (remove whitespace, handle case + # sensitivity if needed) + existing_pgroups = ( + set(p.strip() for p in db_address.pgroups.split(",") if p.strip()) + if db_address.pgroups + else set() + ) + new_pgroups = ( + set(p.strip() for p in address.pgroups.split(",") if p.strip()) + if address.pgroups + else set() + ) + + # Check if any old pgroups are being removed (strict validation against removal) + if not new_pgroups.issuperset(existing_pgroups): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Modifying pgroups to remove existing ones is not allowed.", + ) + + # Combine existing and new pgroups (only additions are allowed) + combined_pgroups = existing_pgroups.union(new_pgroups) + + # Mark the current address as obsolete + db_address.status = "inactive" db.commit() db.refresh(db_address) - return db_address + + # Create a new address with updated values and the combined pgroups + new_address = AddressModel( + pgroups=",".join(combined_pgroups), # Join set back into comma-separated string + house_number=address.house_number or db_address.house_number, + street=address.street or db_address.street, + city=address.city or db_address.city, + state=address.state or db_address.state, + zipcode=address.zipcode or db_address.zipcode, + country=address.country or db_address.country, + status="active", # Newly created address will be active + ) + + # Save the new address + db.add(new_address) + db.commit() + db.refresh(new_address) + + return new_address -@router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT) +@protected_router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_return_address(address_id: int, db: Session = Depends(get_db)): db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first() if not db_address: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Address not found." ) - db.delete(db_address) + + # Mark the address as obsolete instead of deleting it + db_address.status = "inactive" db.commit() return diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 99ad4cf..773f992 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -14,8 +14,13 @@ mock_users_db = { "testuser": { "username": "testuser", "password": "testpass", # In a real scenario, store the hash of the password - "pgroups": [20000, 20001, 20003], - } + "pgroups": ["p20000", "p20001", "p20002", "p20003"], + }, + "testuser2": { + "username": "testuser2", + "password": "testpass2", # In a real scenario, store the hash of the password + "pgroups": ["p20004", "p20005", "p20006"], + }, } @@ -39,30 +44,17 @@ def create_access_token(data: dict) -> str: async def get_current_user(token: str = Depends(oauth2_scheme)) -> loginData: - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - token_expired_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token expired", - headers={"WWW-Authenticate": "Bearer"}, - ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") - pgroups = payload.get("pgroups") - - if username is None: - raise credentials_exception - token_data = loginData(username=username, pgroups=pgroups) + print(f"[DEBUG] Username decoded from token: {username}") # Add debug log here + return loginData(username=username, pgroups=payload.get("pgroups")) except jwt.ExpiredSignatureError: - raise token_expired_exception + print("[DEBUG] Token expired") + raise HTTPException(status_code=401, detail="Token expired") except jwt.InvalidTokenError: - raise credentials_exception - - return token_data + print("[DEBUG] Invalid token") + raise HTTPException(status_code=401, detail="Invalid token") @router.post("/token/login", response_model=loginToken) diff --git a/backend/app/routers/protected_router.py b/backend/app/routers/protected_router.py new file mode 100644 index 0000000..62b0313 --- /dev/null +++ b/backend/app/routers/protected_router.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter, Depends + +from app.routers.auth import get_current_user + +protected_router = APIRouter( + dependencies=[Depends(get_current_user)] # Applies to all routes +) diff --git a/backend/app/routers/sample.py b/backend/app/routers/sample.py index 8fdb6aa..811d428 100644 --- a/backend/app/routers/sample.py +++ b/backend/app/routers/sample.py @@ -7,7 +7,6 @@ import shutil from app.schemas import ( Puck as PuckSchema, Sample as SampleSchema, - SampleEventResponse, SampleEventCreate, Sample, ) @@ -90,106 +89,69 @@ async def create_sample_event( return sample # Return the sample, now including `mount_count` -# Route to fetch the last (most recent) sample event -@router.get("/samples/{sample_id}/events/last", response_model=SampleEventResponse) -async def get_last_sample_event(sample_id: int, db: Session = Depends(get_db)): - # Ensure the sample exists - sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first() - if not sample: - raise HTTPException(status_code=404, detail="Sample not found") - - # Get the most recent event for the sample - last_event = ( - db.query(SampleEventModel) - .filter(SampleEventModel.sample_id == sample_id) - .order_by(SampleEventModel.timestamp.desc()) - .first() - ) - - if not last_event: - raise HTTPException(status_code=404, detail="No events found for the sample") - - return SampleEventResponse( - id=last_event.id, - sample_id=last_event.sample_id, - event_type=last_event.event_type, - timestamp=last_event.timestamp, - ) # 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 + uploaded_files: list[UploadFile] = File(...), 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/. + logging.info(f"Received files: {[file.filename for file in uploaded_files]}") + """ + Uploads images for a given sample and saves them to a directory structure. Args: sample_id (int): ID of the sample. - uploaded_files (List[UploadFile]): List of image files to be uploaded. - db (Session): SQLAlchemy database session. + uploaded_files (list[UploadFile]): A list of files uploaded with the request. + db (Session): Database session. """ - # Fetch sample details from the database + + # 1. Validate Sample 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 + # 2. Define Directory Structure + username = "e16371" # Hardcoded username; replace with dynamic logic if applicable 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) + dewar_name = ( + sample.puck.dewar.dewar_name + if sample.puck and sample.puck.dewar + else "default_dewar" ) - - # Create directories if they don't exist + puck_name = sample.puck.puck_name if sample.puck else "default_puck" + position = sample.position if sample.position else "default_position" + base_dir = Path(f"images/{username}/{today}/{dewar_name}/{puck_name}/{position}") base_dir.mkdir(parents=True, exist_ok=True) - # Save each uploaded image to the directory + # 3. Process and Save Each File + saved_files = [] for file in uploaded_files: - # Validate file content type + # Validate MIME type if not file.content_type.startswith("image/"): raise HTTPException( status_code=400, - detail=f"Invalid file type: {file.filename}. Must be an image.", + detail=f"Invalid file type: {file.filename}. Only images are accepted.", ) - # Create a file path for storing the uploaded file + # Save file to the base directory file_path = base_dir / file.filename + # Save the file from the file stream try: - # Save the file 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"Error saving file {file.filename}: {str(e)}", + detail=f"Could not save file {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}.") return { - "message": f"{len(uploaded_files)} images uploaded successfully.", - "path": str(base_dir), # Return the base directory for reference + "message": f"{len(saved_files)} images uploaded successfully.", + "files": saved_files, } diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 203bcc4..b8e4a53 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -16,7 +16,7 @@ class loginToken(BaseModel): class loginData(BaseModel): username: str - pgroups: List[int] + pgroups: List[str] class DewarTypeBase(BaseModel): @@ -392,22 +392,29 @@ class ContactPersonUpdate(BaseModel): class AddressCreate(BaseModel): + pgroups: str + house_number: Optional[str] = None street: str city: str + state: Optional[str] = None zipcode: str country: str class Address(AddressCreate): id: int + status: str = "active" class Config: from_attributes = True class AddressUpdate(BaseModel): + pgroups: str + house_number: Optional[str] = None street: Optional[str] = None city: Optional[str] = None + state: Optional[str] = None zipcode: Optional[str] = None country: Optional[str] = None diff --git a/backend/main.py b/backend/main.py index 04131e5..b6c62a3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -18,6 +18,7 @@ from app.routers import ( sample, ) from app.database import Base, engine, SessionLocal +from app.routers.protected_router import protected_router # Utility function to fetch metadata from pyproject.toml @@ -139,8 +140,8 @@ def on_startup(): load_slots_data(db) else: # dev or test environments print(f"{environment.capitalize()} environment: Regenerating database.") - # Base.metadata.drop_all(bind=engine) - # Base.metadata.create_all(bind=engine) + Base.metadata.drop_all(bind=engine) + Base.metadata.create_all(bind=engine) if environment == "dev": from app.database import load_sample_data @@ -154,9 +155,10 @@ def on_startup(): # Include routers with correct configuration +app.include_router(protected_router, prefix="/protected", tags=["protected"]) app.include_router(auth.router, prefix="/auth", tags=["auth"]) app.include_router(contact.router, prefix="/contacts", tags=["contacts"]) -app.include_router(address.router, prefix="/addresses", tags=["addresses"]) +app.include_router(address.protected_router, prefix="/addresses", tags=["addresses"]) app.include_router(proposal.router, prefix="/proposals", tags=["proposals"]) app.include_router(dewar.router, prefix="/dewars", tags=["dewars"]) app.include_router(shipment.router, prefix="/shipments", tags=["shipments"]) diff --git a/backend/tests/sample_image/IMG_1942.jpg b/backend/tests/sample_image/IMG_1942.jpg new file mode 100644 index 0000000..05388f1 Binary files /dev/null and b/backend/tests/sample_image/IMG_1942.jpg differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fcd3d53..02969bd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,6 +29,7 @@ "exceljs": "^4.4.0", "file-saver": "^2.0.5", "fuse.js": "^7.0.0", + "jwt-decode": "^4.0.0", "openapi-typescript-codegen": "^0.29.0", "react": "^18.3.1", "react-big-calendar": "^1.15.0", @@ -485,6 +486,15 @@ "@devexpress/dx-core": "4.0.10" } }, + "node_modules/@devexpress/dx-scheduler-core/node_modules/rrule": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.1.tgz", + "integrity": "sha512-4p20u/1U7WqR3Nb1hOUrm0u1nSI7sO93ZUVZEZ5HeF6Gr5OlJuyhwEGRvUHq8ZfrPsq5gfa5b9dqnUs/kPqpIw==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -4732,6 +4742,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5992,10 +6011,12 @@ } }, "node_modules/rrule": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.1.tgz", - "integrity": "sha512-4p20u/1U7WqR3Nb1hOUrm0u1nSI7sO93ZUVZEZ5HeF6Gr5OlJuyhwEGRvUHq8ZfrPsq5gfa5b9dqnUs/kPqpIw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz", + "integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==", "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } diff --git a/frontend/package.json b/frontend/package.json index 2d5a8f2..140f8c3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,7 @@ "exceljs": "^4.4.0", "file-saver": "^2.0.5", "fuse.js": "^7.0.0", + "jwt-decode": "^4.0.0", "openapi-typescript-codegen": "^0.29.0", "react": "^18.3.1", "react-big-calendar": "^1.15.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8ec2c00..04170ca 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { setUpToken, clearToken } from './utils/auth'; // Import the token utilities import ResponsiveAppBar from './components/ResponsiveAppBar'; import ShipmentView from './pages/ShipmentView'; @@ -16,38 +17,77 @@ const App: React.FC = () => { const [openAddressManager, setOpenAddressManager] = useState(false); const [openContactsManager, setOpenContactsManager] = useState(false); - const handleOpenAddressManager = () => { - setOpenAddressManager(true); - }; + const handleOpenAddressManager = () => setOpenAddressManager(true); + const handleCloseAddressManager = () => setOpenAddressManager(false); + const handleOpenContactsManager = () => setOpenContactsManager(true); + const handleCloseContactsManager = () => setOpenContactsManager(false); - const handleCloseAddressManager = () => { - setOpenAddressManager(false); - }; + const [pgroups, setPgroups] = useState([]); + const [activePgroup, setActivePgroup] = useState(''); - const handleOpenContactsManager = () => { - setOpenContactsManager(true); - }; + // On app load, configure the token + useEffect(() => { + setUpToken(); // Ensure token is loaded into OpenAPI on app initialization + }, []); - const handleCloseContactsManager = () => { - setOpenContactsManager(false); + useEffect(() => { + const updateStateFromLocalStorage = () => { + const user = localStorage.getItem('user'); + console.log("[DEBUG] User data in localStorage (update):", user); // Debug + if (user) { + try { + const parsedUser = JSON.parse(user); + if (parsedUser.pgroups && Array.isArray(parsedUser.pgroups)) { + setPgroups(parsedUser.pgroups); + setActivePgroup(parsedUser.pgroups[0] || ''); + console.log("[DEBUG] Pgroups updated in state:", parsedUser.pgroups); + } else { + console.warn("[DEBUG] No pgroups found in user data"); + } + } catch (error) { + console.error("[DEBUG] Error parsing user data:", error); + } + } else { + console.warn("[DEBUG] No user in localStorage"); + } + }; + + // Run on component mount + updateStateFromLocalStorage(); + + // Listen for localStorage changes + window.addEventListener('storage', updateStateFromLocalStorage); + + // Cleanup listener on unmount + return () => { + window.removeEventListener('storage', updateStateFromLocalStorage); + }; + }, []); + + const handlePgroupChange = (newPgroup: string) => { + setActivePgroup(newPgroup); + console.log(`pgroup changed to: ${newPgroup}`); }; return ( } /> } />} /> - } />} /> + } />} /> } />} /> } />} /> - {/* Other routes as necessary */} - + diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index 4479334..1b4769c 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -10,7 +10,7 @@ interface ModalProps { const Modal: React.FC = ({ open, onClose, title, children }) => { return ( - + {title} {children} diff --git a/frontend/src/components/ResponsiveAppBar.tsx b/frontend/src/components/ResponsiveAppBar.tsx index a2aefc0..e2d5367 100644 --- a/frontend/src/components/ResponsiveAppBar.tsx +++ b/frontend/src/components/ResponsiveAppBar.tsx @@ -7,25 +7,43 @@ import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; -import MenuIcon from '@mui/icons-material/Menu'; import Container from '@mui/material/Container'; import Avatar from '@mui/material/Avatar'; import Tooltip from '@mui/material/Tooltip'; -import { Button } from '@mui/material'; +import { Button, Select, FormControl, InputLabel, } from '@mui/material'; import { Link } from 'react-router-dom'; import logo from '../assets/icons/psi_01_sn.svg'; import '../App.css'; +import { clearToken } from '../utils/auth'; + interface ResponsiveAppBarProps { + activePgroup: string; onOpenAddressManager: () => void; onOpenContactsManager: () => void; + pgroups: string[]; // Pass the pgroups from the server + currentPgroup: string; // Currently selected pgroup + onPgroupChange: (pgroup: string) => void; // Callback when selected pgroup changes } -const ResponsiveAppBar: React.FC = ({ onOpenAddressManager, onOpenContactsManager }) => { +const ResponsiveAppBar: React.FC = ({ + activePgroup, + onOpenAddressManager, + onOpenContactsManager, + pgroups, + currentPgroup, + onPgroupChange }) => { const navigate = useNavigate(); const location = useLocation(); const [anchorElNav, setAnchorElNav] = useState(null); const [anchorElUser, setAnchorElUser] = useState(null); + const [selectedPgroup, setSelectedPgroup] = useState(currentPgroup); + console.log('Active Pgroup:', activePgroup); + const handlePgroupChange = (event: React.ChangeEvent<{ value: unknown }>) => { + const newPgroup = event.target.value as string; + setSelectedPgroup(newPgroup); + onPgroupChange(newPgroup); // Inform parent about the change + }; const pages = [ { name: 'Home', path: '/' }, @@ -59,8 +77,8 @@ const ResponsiveAppBar: React.FC = ({ onOpenAddressManage const handleLogout = () => { console.log("Performing logout..."); - localStorage.removeItem('token'); - navigate('/login'); + clearToken(); // Clear the token from localStorage and OpenAPI + navigate('/login'); // Redirect to login page }; const handleMenuItemClick = (action: string) => { @@ -123,7 +141,31 @@ const ResponsiveAppBar: React.FC = ({ onOpenAddressManage ))} - + + + pgroup + + + diff --git a/frontend/src/components/ShipmentDetails.tsx b/frontend/src/components/ShipmentDetails.tsx index 03bad09..0e232ba 100644 --- a/frontend/src/components/ShipmentDetails.tsx +++ b/frontend/src/components/ShipmentDetails.tsx @@ -12,6 +12,7 @@ import DewarDetails from './DewarDetails'; const MAX_COMMENTS_LENGTH = 200; interface ShipmentDetailsProps { + activePgroup: string; isCreatingShipment: boolean; sx?: SxProps; selectedShipment: Shipment | null; @@ -23,6 +24,7 @@ interface ShipmentDetailsProps { } const ShipmentDetails: React.FC = ({ + activePgroup, sx, selectedShipment, setSelectedDewar, @@ -34,6 +36,8 @@ const ShipmentDetails: React.FC = ({ const [comments, setComments] = useState(selectedShipment?.comments || ''); const [initialComments, setInitialComments] = useState(selectedShipment?.comments || ''); + console.log('Active Pgroup:', activePgroup); // Debugging or use it where required + const initialNewDewarState: Partial = { dewar_name: '', tracking_number: '', diff --git a/frontend/src/components/ShipmentForm.tsx b/frontend/src/components/ShipmentForm.tsx index dfa5a02..4379237 100644 --- a/frontend/src/components/ShipmentForm.tsx +++ b/frontend/src/components/ShipmentForm.tsx @@ -6,15 +6,18 @@ import { import { SelectChangeEvent } from '@mui/material'; import { SxProps } from '@mui/system'; import { - ContactPersonCreate, ContactPerson, Address, Proposal, ContactsService, AddressesService, AddressCreate, ProposalsService, + ContactPersonCreate, ContactPerson, Address, AddressCreate, Proposal, ContactsService, AddressesService, ProposalsService, OpenAPI, ShipmentCreate, ShipmentsService } from '../../openapi'; import { useEffect } from 'react'; import { CountryList } from './CountryList'; // Import the list of countries +import { jwtDecode } from 'jwt-decode'; + const MAX_COMMENTS_LENGTH = 200; interface ShipmentFormProps { + activePgroup: string; sx?: SxProps; onCancel: () => void; refreshShipments: () => void; @@ -26,7 +29,17 @@ const fuse = new Fuse(CountryList, { includeScore: true, }); -const ShipmentForm: React.FC = ({ sx = {}, onCancel, refreshShipments }) => { +const token = localStorage.getItem('token'); + +if (token) { + OpenAPI.TOKEN = token; // Ensure OpenAPI client uses this token +} + +const ShipmentForm: React.FC = ({ + activePgroup, + sx = {}, + onCancel, + refreshShipments }) => { const [countrySuggestions, setCountrySuggestions] = React.useState([]); const [contactPersons, setContactPersons] = React.useState([]); const [returnAddresses, setReturnAddresses] = React.useState([]); @@ -37,7 +50,7 @@ const ShipmentForm: React.FC = ({ sx = {}, onCancel, refreshS firstname: '', lastname: '', phone_number: '', email: '' }); const [newReturnAddress, setNewReturnAddress] = React.useState>({ - street: '', city: '', zipcode: '', country: '' + pgroup:'', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' }); const [newShipment, setNewShipment] = React.useState>({ shipment_name: '', shipment_status: 'In preparation', comments: '' @@ -80,12 +93,23 @@ const ShipmentForm: React.FC = ({ sx = {}, onCancel, refreshS }; const getAddresses = async () => { + if (!activePgroup) { + console.error("Active pgroup is missing."); + setErrorMessage("Active pgroup is missing. Unable to load addresses."); + return; + } + try { + // Pass activePgroup directly as a string (not as an object) const fetchedAddresses: Address[] = - await AddressesService.getReturnAddressesAddressesGet(); + await AddressesService.getReturnAddressesAddressesGet(activePgroup); + setReturnAddresses(fetchedAddresses); - } catch { - setErrorMessage('Failed to load return addresses.'); + } catch (error) { + console.error("Error fetching addresses:", error); + + // Extract and log meaningful information from OpenAPI errors (if available) + setErrorMessage("Failed to load return addresses due to API error."); } }; @@ -102,7 +126,7 @@ const ShipmentForm: React.FC = ({ sx = {}, onCancel, refreshS getContacts(); getAddresses(); getProposals(); - }, []); + }, [activePgroup]); const handleCountryInputChange = (event: React.ChangeEvent) => { const value = event.target.value; @@ -170,13 +194,14 @@ const ShipmentForm: React.FC = ({ sx = {}, onCancel, refreshS const payload: ShipmentCreate = { shipment_name: newShipment.shipment_name || '', - shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required at all + shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required shipment_status: newShipment.shipment_status || 'In preparation', comments: newShipment.comments || '', contact_person_id: selectedContactPersonId!, return_address_id: selectedReturnAddressId!, proposal_id: selectedProposalId!, - dewars: newShipment.dewars || [] + dewars: newShipment.dewars || [], + //pgroup: activePgroup, }; console.log('Shipment Payload being sent:', payload); @@ -249,32 +274,44 @@ const ShipmentForm: React.FC = ({ sx = {}, onCancel, refreshS }; const handleSaveNewReturnAddress = async () => { - if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city || - !newReturnAddress.country) { + // Validate address form data + if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city || !newReturnAddress.country) { setErrorMessage('Please fill in all new return address fields correctly.'); return; } + // Ensure activePgroup is available + if (!activePgroup) { + setErrorMessage('Active pgroup is missing. Please try again.'); + return; + } + + // Construct the payload const payload: AddressCreate = { + pgroups: activePgroup, // Use the activePgroup prop directly + house_number: newReturnAddress.house_number, street: newReturnAddress.street, city: newReturnAddress.city, + state: newReturnAddress.state, zipcode: newReturnAddress.zipcode, country: newReturnAddress.country, }; console.log('Return Address Payload being sent:', payload); + // Call the API with the completed payload try { const response: Address = await AddressesService.createReturnAddressAddressesPost(payload); - setReturnAddresses([...returnAddresses, response]); + setReturnAddresses([...returnAddresses, response]); // Update the address state setErrorMessage(null); - setSelectedReturnAddressId(response.id); + setSelectedReturnAddressId(response.id); // Set the newly created address ID to the form } catch (error) { console.error('Failed to create a new return address:', error); setErrorMessage('Failed to create a new return address. Please try again later.'); } - setNewReturnAddress({ street: '', city: '', zipcode: '', country: '' }); + // Reset form inputs and close the "Create New Address" form + setNewReturnAddress({ pgroup: '', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' }); setIsCreatingReturnAddress(false); }; @@ -390,11 +427,22 @@ const ShipmentForm: React.FC = ({ sx = {}, onCancel, refreshS onChange={handleReturnAddressChange} displayEmpty > - {returnAddresses.map((address) => ( - - {`${address.street}, ${address.city}, ${address.zipcode}, ${address.country}`} - - ))} + {returnAddresses.map((address) => { + const addressParts = [ + address.house_number, + address.street, + address.city, + address.zipcode, + address.state, + address.country + ].filter(part => part); // Remove falsy (null/undefined/empty) values + + return ( + + {addressParts.join(', ')} {/* Join the valid address parts with a comma */} + + ); + })} Create New Return Address @@ -402,6 +450,7 @@ const ShipmentForm: React.FC = ({ sx = {}, onCancel, refreshS {isCreatingReturnAddress && ( <> + = ({ sx = {}, onCancel, refreshS fullWidth required /> + setNewReturnAddress({ ...newReturnAddress, house_number: e.target.value })} + fullWidth + /> = ({ sx = {}, onCancel, refreshS fullWidth required /> + setNewReturnAddress({ ...newReturnAddress, state: e.target.value })} + fullWidth + /> void; + activePgroup: string; selectShipment: (shipment: Shipment | null) => void; selectedShipment: Shipment | null; sx?: SxProps; @@ -27,6 +28,7 @@ const statusIconMap: Record = { }; const ShipmentPanel: React.FC = ({ + activePgroup, setCreatingShipment, selectShipment, selectedShipment, @@ -37,6 +39,8 @@ const ShipmentPanel: React.FC = ({ }) => { const [uploadDialogOpen, setUploadDialogOpen] = useState(false); + console.log('Active Pgroup:', activePgroup); + const handleDeleteShipment = async () => { if (selectedShipment) { const confirmDelete = window.confirm( diff --git a/frontend/src/pages/AddressManagerView.tsx b/frontend/src/pages/AddressManagerView.tsx index 46d9468..cc6c9cb 100644 --- a/frontend/src/pages/AddressManagerView.tsx +++ b/frontend/src/pages/AddressManagerView.tsx @@ -2,26 +2,57 @@ import React from 'react'; import Fuse from 'fuse.js'; import { CountryList } from '../components/CountryList'; import { - Container, Typography, List, ListItem, IconButton, TextField, Box, ListItemText, ListItemSecondaryAction, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button + Container, + Typography, + List, + ListItem, + IconButton, + TextField, + Box, + ListItemText, + ListItemSecondaryAction, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Button, + Chip } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import SaveIcon from '@mui/icons-material/Save'; import AddIcon from '@mui/icons-material/Add'; -import { AddressesService } from '../../openapi'; -import type { Address, AddressCreate, AddressUpdate } from '../models/Address'; +import {Address, AddressCreate, AddressesService, AddressUpdate} from '../../openapi'; + +interface AddressManagerProps { + pgroups: string[]; + activePgroup: string; +} + + +// Extend the generated Address type +interface AddressWithPgroups extends Address { + associatedPgroups: string[]; // Dynamically added pgroups +} + const fuse = new Fuse(CountryList, { threshold: 0.3, // Lower threshold for stricter matches includeScore: true, }); -const AddressManager: React.FC = () => { +const AddressManager: React.FC = ({ pgroups, activePgroup }) => { + // Use pgroups and activePgroup directly + console.log('User pgroups:', pgroups); + console.log('Active pgroup:', activePgroup); const [countrySuggestions, setCountrySuggestions] = React.useState([]); const [addresses, setAddresses] = React.useState([]); const [newAddress, setNewAddress] = React.useState>({ + house_number: '', street: '', city: '', + state: '', zipcode: '', country: '', }); @@ -45,18 +76,28 @@ const AddressManager: React.FC = () => { }; React.useEffect(() => { - const fetchAddresses = async () => { + const fetchAllData = async () => { try { - const response = await AddressesService.getReturnAddressesAddressesGet(); - setAddresses(response); + const response = await AddressesService.getAllAddressesAddressesAllGet(); + + // Preprocess: Add associated and unassociated pgroups + const transformedAddresses = response.map((address) => { + const addressPgroups = address.pgroups?.split(',').map((p) => p.trim()) || []; + const associatedPgroups = pgroups.filter((pgroup) => addressPgroups.includes(pgroup)); + return { + ...address, + associatedPgroups, // pgroups linked to the address + }; + }); + + setAddresses(transformedAddresses); } catch (error) { console.error('Failed to fetch addresses', error); - setErrorMessage('Failed to load addresses. Please try again later.'); + setErrorMessage('Failed to load addresses. Please try again.'); } }; - - fetchAddresses(); - }, []); + fetchAllData(); + }, [pgroups]); const handleInputChange = (event: React.ChangeEvent) => { const { name, value } = event.target; @@ -66,30 +107,40 @@ const AddressManager: React.FC = () => { const handleAddOrUpdateAddress = async () => { try { if (editAddressId !== null) { - // Update address - await AddressesService.updateReturnAddressAddressesAddressIdPut(editAddressId, newAddress as AddressUpdate); - setAddresses(addresses.map(address => address.id === editAddressId ? { ...address, ...newAddress } : address)); + // Update address (mark old one obsolete, create a new one) + const updatedAddress = await AddressesService.updateReturnAddressAddressesAddressIdPut( + editAddressId, + newAddress as AddressUpdate + ); + + // Replace old address with the new one in the list + setAddresses(addresses.map(address => + address.id === editAddressId ? updatedAddress : address + ).filter(address => address.status === "active")); // Keep only active addresses setEditAddressId(null); } else { // Add new address const response = await AddressesService.createReturnAddressAddressesPost(newAddress as AddressCreate); setAddresses([...addresses, response]); } - setNewAddress({ street: '', city: '', zipcode: '', country: '' }); + setNewAddress({ house_number:'', street: '', city: '', state: '', zipcode: '', country: '' }); setErrorMessage(null); } catch (error) { console.error('Failed to add/update address', error); - setErrorMessage('Failed to add/update address. Please try again later.'); + setErrorMessage('Failed to add/update address. Please try again.'); } }; const handleDeleteAddress = async (id: number) => { try { + // Delete (inactivate) the address await AddressesService.deleteReturnAddressAddressesAddressIdDelete(id); - setAddresses(addresses.filter(address => address.id !== id)); + + // Remove the obsolete address from the active list in the UI + setAddresses(addresses.filter(address => address.id !== id && address.status === "active")); } catch (error) { console.error('Failed to delete address', error); - setErrorMessage('Failed to delete address. Please try again later.'); + setErrorMessage('Failed to delete address. Please try again.'); } }; @@ -115,52 +166,149 @@ const AddressManager: React.FC = () => { } }; + const togglePgroupAssociation = async (addressId: number, pgroup: string) => { + try { + const address = addresses.find((addr) => addr.id === addressId); + if (!address) return; + + const isAssociated = address.associatedPgroups.includes(pgroup); + + // Only allow adding a pgroup + if (isAssociated) { + console.warn('Removing a pgroup is not allowed.'); + return; + } + + const updatedPgroups = [...address.associatedPgroups, pgroup]; // Add the pgroup + + // Update the backend + await AddressesService.updateReturnAddressAddressesAddressIdPut(addressId, { + ...address, + pgroups: updatedPgroups.join(','), // Sync updated pgroups + }); + + // Update address in local state + setAddresses((prevAddresses) => + prevAddresses.map((addr) => + addr.id === addressId + ? { ...addr, associatedPgroups: updatedPgroups } + : addr + ) + ); + } catch (error) { + console.error('Failed to add pgroup association', error); + setErrorMessage('Failed to add pgroup association. Please try again.'); + } + }; + + const renderPgroupChips = (address: AddressWithPgroups) => { + return pgroups.map((pgroup) => { + const isAssociated = address.associatedPgroups.includes(pgroup); + return ( + togglePgroupAssociation(address.id, pgroup) + : undefined + } + sx={{ + backgroundColor: isAssociated ? '#19d238' : '#b0b0b0', + color: 'white', + borderRadius: '8px', + fontWeight: 'bold', + height: '20px', + fontSize: '12px', + boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.2)', + cursor: isAssociated ? 'default' : 'pointer', // Disable pointer for associated chips + '&:hover': { opacity: isAssociated ? 1 : 0.8 }, // Disable hover effect for associated chips + mr: 1, + mb: 1, + }} + /> + ); + }); + }; + return ( Addresses Management - - - - + - - {/* Render suggestions dynamically */} - + + + + + + {/* Country field dynamically takes available space */} - + {/* Render suggestions dynamically */} {countrySuggestions.length > 0 && ( {countrySuggestions.map((suggestion, index) => ( @@ -172,7 +320,6 @@ const AddressManager: React.FC = () => { '&:hover': { background: '#f5f5f5' }, }} onClick={() => { - // Update country field with the clicked suggestion setNewAddress({ ...newAddress, country: suggestion }); setCountrySuggestions([]); // Clear suggestions }} @@ -193,14 +340,18 @@ const AddressManager: React.FC = () => { addresses.map((address) => ( + {renderPgroupChips(address)} + + } /> handleEditAddress(address)}> - openDialog(address)}> + handleDeleteAddress(address.id)}> diff --git a/frontend/src/pages/LoginView.tsx b/frontend/src/pages/LoginView.tsx index 048d50f..8c1f490 100644 --- a/frontend/src/pages/LoginView.tsx +++ b/frontend/src/pages/LoginView.tsx @@ -89,10 +89,25 @@ const LoginView: React.FC = () => { password: password, }); + // Save the token localStorage.setItem('token', response.access_token); - navigate('/'); // Redirect post-login + OpenAPI.TOKEN = response.access_token; + + // Decode token to extract user data (e.g., pgroups) + const decodedToken = JSON.parse(atob(response.access_token.split('.')[1])); // Decode JWT payload + const userData = { + username: decodedToken.sub, + pgroups: decodedToken.pgroups || [], // Ensure pgroups is an array + }; + localStorage.setItem('user', JSON.stringify(userData)); // Save user data in localStorage + + console.log("Token updated successfully:", response.access_token); + console.log("User data saved:", userData); + + navigate('/'); // Redirect after successful login } catch (err) { setError('Login failed. Please check your credentials.'); + console.error("Error during login:", err); } }; diff --git a/frontend/src/pages/ShipmentView.tsx b/frontend/src/pages/ShipmentView.tsx index 8095557..02127b3 100644 --- a/frontend/src/pages/ShipmentView.tsx +++ b/frontend/src/pages/ShipmentView.tsx @@ -6,14 +6,20 @@ import { Dewar, OpenAPI, Shipment } from '../../openapi'; import useShipments from '../hooks/useShipments'; import { Grid, Container } from '@mui/material'; -type ShipmentViewProps = React.PropsWithChildren>; +type ShipmentViewProps = { + activePgroup: string; +}; -const ShipmentView: React.FC = () => { +const ShipmentView: React.FC = ( { activePgroup }) => { const { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments(); const [selectedShipment, setSelectedShipment] = useState(null); const [selectedDewar, setSelectedDewar] = useState(null); const [isCreatingShipment, setIsCreatingShipment] = useState(false); + useEffect(() => { + fetchAndSetShipments(); + }, [activePgroup]); + useEffect(() => { // Detect the current environment const mode = import.meta.env.MODE; @@ -52,6 +58,7 @@ const ShipmentView: React.FC = () => { if (isCreatingShipment) { return ( = () => { if (selectedShipment) { return ( = () => { }} > { + const token = localStorage.getItem('token'); + if (token) { + OpenAPI.TOKEN = token; // Assign the token to OpenAPI client + } else { + console.warn("No token found in localStorage."); + } +}; + +export const clearToken = (): void => { + localStorage.removeItem('token'); + OpenAPI.TOKEN = ''; // Clear the token from the OpenAPI client + console.log("Token cleared from OpenAPI and localStorage."); +}; \ No newline at end of file diff --git a/testfunctions.ipynb b/testfunctions.ipynb index b0ef502..d44191c 100644 --- a/testfunctions.ipynb +++ b/testfunctions.ipynb @@ -6,20 +6,20 @@ "metadata": { "collapsed": true, "ExecuteTime": { - "end_time": "2025-01-17T14:03:52.460891Z", - "start_time": "2025-01-17T14:03:52.454842Z" + "end_time": "2025-01-20T14:44:37.526742Z", + "start_time": "2025-01-20T14:44:37.522704Z" } }, "source": [ "import json\n", "\n", - "from nbclient.client import timestamp\n", + "#from nbclient.client import timestamp\n", "\n", "import backend.aareDBclient as aareDBclient\n", "from aareDBclient.rest import ApiException\n", "from pprint import pprint\n", "\n", - "from app.data.data import sample\n", + "#from app.data.data import sample\n", "\n", "#from aareDBclient import SamplesApi, ShipmentsApi, PucksApi\n", "#from aareDBclient.models import SampleEventCreate, SetTellPosition\n", @@ -47,13 +47,13 @@ ] } ], - "execution_count": 40 + "execution_count": 2 }, { "metadata": { "ExecuteTime": { - "end_time": "2025-01-17T14:13:16.559702Z", - "start_time": "2025-01-17T14:13:16.446021Z" + "end_time": "2025-01-20T14:44:58.875370Z", + "start_time": "2025-01-20T14:44:58.805520Z" } }, "cell_type": "code", @@ -152,7 +152,7 @@ ] } ], - "execution_count": 44 + "execution_count": 3 }, { "metadata": { @@ -456,8 +456,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-01-20T11:10:20.017201Z", - "start_time": "2025-01-20T11:10:19.953940Z" + "end_time": "2025-01-20T14:45:11.812597Z", + "start_time": "2025-01-20T14:45:11.793309Z" } }, "cell_type": "code", @@ -503,7 +503,9 @@ "output_type": "stream", "text": [ "Payload being sent to API:\n", - "{\"event_type\":\"Mounted\"}\n" + "{\"event_type\":\"Mounted\"}\n", + "API response:\n", + "Sample(id=433, sample_name='Dtpase_1', position=1, puck_id=44, crystalname=None, proteinname=None, positioninpuck=None, priority=1, comments=None, data_collection_parameters=DataCollectionParameters(directory='{sgPuck}/{sgPosition}', oscillation=None, exposure=None, totalrange=None, transmission=None, targetresolution=None, aperture=None, datacollectiontype=None, processingpipeline='', spacegroupnumber=None, cellparameters=None, rescutkey=None, rescutvalue=None, userresolution=None, pdbid='', autoprocfull=False, procfull=False, adpenabled=False, noano=False, ffcscampaign=False, trustedhigh=None, autoprocextraparams=None, chiphiangles=None, dose=None), events=[SampleEventResponse(id=386, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 35, 38)), SampleEventResponse(id=387, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 40, 11)), SampleEventResponse(id=388, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 45, 4)), SampleEventResponse(id=389, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 45, 24)), SampleEventResponse(id=390, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 50, 38)), SampleEventResponse(id=391, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 52, 28)), SampleEventResponse(id=392, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 12, 10, 20)), SampleEventResponse(id=393, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 13, 39, 24)), SampleEventResponse(id=394, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 15, 45, 12))], mount_count=9, unmount_count=0)\n" ] }, { @@ -513,29 +515,9 @@ "/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": "ValidationError", - "evalue": "3 validation errors for SampleEventResponse\nsample_id\n Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/int_type\nevent_type\n Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/string_type\ntimestamp\n Input should be a valid datetime [type=datetime_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/datetime_type", - "output_type": "error", - "traceback": [ - "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[0;31mValidationError\u001B[0m Traceback (most recent call last)", - "Cell \u001B[0;32mIn[110], line 21\u001B[0m\n\u001B[1;32m 18\u001B[0m \u001B[38;5;28mprint\u001B[39m(sample_event_create\u001B[38;5;241m.\u001B[39mjson()) \u001B[38;5;66;03m# Ensure it matches `SampleEventCreate`\u001B[39;00m\n\u001B[1;32m 20\u001B[0m \u001B[38;5;66;03m# Call the API\u001B[39;00m\n\u001B[0;32m---> 21\u001B[0m api_response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_instance\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mcreate_sample_event_samples_samples_sample_id_events_post\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 22\u001B[0m \u001B[43m \u001B[49m\u001B[43msample_id\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[38;5;241;43m433\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;66;43;03m# Ensure this matches a valid sample ID in the database\u001B[39;49;00m\n\u001B[1;32m 23\u001B[0m \u001B[43m \u001B[49m\u001B[43msample_event_create\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43msample_event_create\u001B[49m\n\u001B[1;32m 24\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 26\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 27\u001B[0m pprint(api_response)\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:109\u001B[0m, in \u001B[0;36mSamplesApi.create_sample_event_samples_samples_sample_id_events_post\u001B[0;34m(self, sample_id, sample_event_create, _request_timeout, _request_auth, _content_type, _headers, _host_index)\u001B[0m\n\u001B[1;32m 104\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 105\u001B[0m \u001B[38;5;241m*\u001B[39m_param,\n\u001B[1;32m 106\u001B[0m _request_timeout\u001B[38;5;241m=\u001B[39m_request_timeout\n\u001B[1;32m 107\u001B[0m )\n\u001B[1;32m 108\u001B[0m response_data\u001B[38;5;241m.\u001B[39mread()\n\u001B[0;32m--> 109\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[43mresponse_deserialize\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 110\u001B[0m \u001B[43m \u001B[49m\u001B[43mresponse_data\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mresponse_data\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 111\u001B[0m \u001B[43m \u001B[49m\u001B[43mresponse_types_map\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_response_types_map\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 112\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\u001B[38;5;241m.\u001B[39mdata\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:319\u001B[0m, in \u001B[0;36mApiClient.response_deserialize\u001B[0;34m(self, response_data, response_types_map)\u001B[0m\n\u001B[1;32m 317\u001B[0m encoding \u001B[38;5;241m=\u001B[39m match\u001B[38;5;241m.\u001B[39mgroup(\u001B[38;5;241m1\u001B[39m) \u001B[38;5;28;01mif\u001B[39;00m match \u001B[38;5;28;01melse\u001B[39;00m \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mutf-8\u001B[39m\u001B[38;5;124m\"\u001B[39m\n\u001B[1;32m 318\u001B[0m response_text \u001B[38;5;241m=\u001B[39m response_data\u001B[38;5;241m.\u001B[39mdata\u001B[38;5;241m.\u001B[39mdecode(encoding)\n\u001B[0;32m--> 319\u001B[0m return_data \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mdeserialize\u001B[49m\u001B[43m(\u001B[49m\u001B[43mresponse_text\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mresponse_type\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mcontent_type\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 320\u001B[0m \u001B[38;5;28;01mfinally\u001B[39;00m:\n\u001B[1;32m 321\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m \u001B[38;5;241m200\u001B[39m \u001B[38;5;241m<\u001B[39m\u001B[38;5;241m=\u001B[39m response_data\u001B[38;5;241m.\u001B[39mstatus \u001B[38;5;241m<\u001B[39m\u001B[38;5;241m=\u001B[39m \u001B[38;5;241m299\u001B[39m:\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:420\u001B[0m, in \u001B[0;36mApiClient.deserialize\u001B[0;34m(self, response_text, response_type, content_type)\u001B[0m\n\u001B[1;32m 414\u001B[0m \u001B[38;5;28;01melse\u001B[39;00m:\n\u001B[1;32m 415\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m ApiException(\n\u001B[1;32m 416\u001B[0m status\u001B[38;5;241m=\u001B[39m\u001B[38;5;241m0\u001B[39m,\n\u001B[1;32m 417\u001B[0m reason\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mUnsupported content type: \u001B[39m\u001B[38;5;132;01m{0}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;241m.\u001B[39mformat(content_type)\n\u001B[1;32m 418\u001B[0m )\n\u001B[0;32m--> 420\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[43m__deserialize\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdata\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mresponse_type\u001B[49m\u001B[43m)\u001B[49m\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:467\u001B[0m, in \u001B[0;36mApiClient.__deserialize\u001B[0;34m(self, data, klass)\u001B[0m\n\u001B[1;32m 465\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m__deserialize_enum(data, klass)\n\u001B[1;32m 466\u001B[0m \u001B[38;5;28;01melse\u001B[39;00m:\n\u001B[0;32m--> 467\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[43m__deserialize_model\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdata\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mklass\u001B[49m\u001B[43m)\u001B[49m\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:788\u001B[0m, in \u001B[0;36mApiClient.__deserialize_model\u001B[0;34m(self, data, klass)\u001B[0m\n\u001B[1;32m 780\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21m__deserialize_model\u001B[39m(\u001B[38;5;28mself\u001B[39m, data, klass):\n\u001B[1;32m 781\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Deserializes list or dict to model.\u001B[39;00m\n\u001B[1;32m 782\u001B[0m \n\u001B[1;32m 783\u001B[0m \u001B[38;5;124;03m :param data: dict, list.\u001B[39;00m\n\u001B[1;32m 784\u001B[0m \u001B[38;5;124;03m :param klass: class literal.\u001B[39;00m\n\u001B[1;32m 785\u001B[0m \u001B[38;5;124;03m :return: model object.\u001B[39;00m\n\u001B[1;32m 786\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m--> 788\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mklass\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mfrom_dict\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdata\u001B[49m\u001B[43m)\u001B[49m\n", - "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/models/sample_event_response.py:86\u001B[0m, in \u001B[0;36mSampleEventResponse.from_dict\u001B[0;34m(cls, obj)\u001B[0m\n\u001B[1;32m 83\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m \u001B[38;5;28misinstance\u001B[39m(obj, \u001B[38;5;28mdict\u001B[39m):\n\u001B[1;32m 84\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mcls\u001B[39m\u001B[38;5;241m.\u001B[39mmodel_validate(obj)\n\u001B[0;32m---> 86\u001B[0m _obj \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mcls\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mmodel_validate\u001B[49m\u001B[43m(\u001B[49m\u001B[43m{\u001B[49m\n\u001B[1;32m 87\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mid\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mid\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m)\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 88\u001B[0m \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[43mobj\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget\u001B[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\n\u001B[1;32m 89\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mevent_type\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mevent_type\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m)\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 90\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mtimestamp\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mtimestamp\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m)\u001B[49m\n\u001B[1;32m 91\u001B[0m \u001B[43m\u001B[49m\u001B[43m}\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 92\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m _obj\n", - "File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/main.py:596\u001B[0m, in \u001B[0;36mBaseModel.model_validate\u001B[0;34m(cls, obj, strict, from_attributes, context)\u001B[0m\n\u001B[1;32m 594\u001B[0m \u001B[38;5;66;03m# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks\u001B[39;00m\n\u001B[1;32m 595\u001B[0m __tracebackhide__ \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;01mTrue\u001B[39;00m\n\u001B[0;32m--> 596\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mcls\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\n\u001B[1;32m 597\u001B[0m \u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mstrict\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mstrict\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mfrom_attributes\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mfrom_attributes\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mcontext\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mcontext\u001B[49m\n\u001B[1;32m 598\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n", - "\u001B[0;31mValidationError\u001B[0m: 3 validation errors for SampleEventResponse\nsample_id\n Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/int_type\nevent_type\n Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/string_type\ntimestamp\n Input should be a valid datetime [type=datetime_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/datetime_type" - ] } ], - "execution_count": 110 + "execution_count": 4 }, { "metadata": { @@ -575,6 +557,126 @@ } ], "execution_count": 7 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-20T15:43:54.575154Z", + "start_time": "2025-01-20T15:43:54.539295Z" + } + }, + "cell_type": "code", + "source": [ + "from aareDBclient import ApiClient, SamplesApi # Import the appropriate client\n", + "from aareDBclient.rest import ApiException\n", + "import mimetypes\n", + "\n", + "# File path to the image\n", + "file_path = \"backend/tests/sample_image/IMG_1942.jpg\"\n", + "\n", + "# Sample ID\n", + "sample_id = 433 # Replace with a valid sample_id from your FastAPI backend\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", + "\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", + "\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", + "\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}\")" + ], + "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[77], 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:875\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 \u001B[38;5;124;03m 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.\u001B[39;00m\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 872\u001B[0m \u001B[38;5;124;03m :return: Returns the result object.\u001B[39;00m\n\u001B[1;32m 873\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m \u001B[38;5;66;03m# noqa: E501\u001B[39;00m\n\u001B[0;32m--> 875\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 876\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 877\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 878\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 879\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 880\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 881\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 882\u001B[0m \u001B[43m \u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 884\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 885\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 886\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 887\u001B[0m }\n\u001B[1;32m 888\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 889\u001B[0m \u001B[38;5;241m*\u001B[39m_param,\n\u001B[1;32m 890\u001B[0m _request_timeout\u001B[38;5;241m=\u001B[39m_request_timeout\n\u001B[1;32m 891\u001B[0m )\n", + "File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api/samples_api.py:1099\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 1095\u001B[0m \u001B[38;5;66;03m# authentication setting\u001B[39;00m\n\u001B[1;32m 1096\u001B[0m _auth_settings: List[\u001B[38;5;28mstr\u001B[39m] \u001B[38;5;241m=\u001B[39m [\n\u001B[1;32m 1097\u001B[0m ]\n\u001B[0;32m-> 1099\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 1100\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 1101\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 1102\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 1103\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 1104\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 1105\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 1106\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 1107\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 1108\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 1109\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 1110\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 1111\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 1112\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": 77 + }, + { + "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", + "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" + ] + } + ], + "execution_count": 51 } ], "metadata": {