from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form from sqlalchemy.orm import Session from pathlib import Path from typing import List from datetime import datetime import shutil from app.schemas import ( Puck as PuckSchema, Sample as SampleSchema, SampleEventCreate, Sample, Image, ImageCreate, SampleResult, ExperimentParametersCreate, ExperimentParametersRead, ImageInfo, ResultResponse, ResultCreate, ) from app.models import ( Puck as PuckModel, Sample as SampleModel, SampleEvent as SampleEventModel, Image as ImageModel, Dewar as DewarModel, ExperimentParameters as ExperimentParametersModel, # ExperimentParameters, Results as ResultsModel, ) from app.dependencies import get_db import logging from sqlalchemy.orm import joinedload router = APIRouter() @router.get("/{puck_id}/samples", response_model=List[SampleSchema]) async def get_samples_with_events(puck_id: int, db: Session = Depends(get_db)): puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() if not puck: raise HTTPException(status_code=404, detail="Puck not found") samples = db.query(SampleModel).filter(SampleModel.puck_id == puck_id).all() for sample in samples: sample.events = ( db.query(SampleEventModel) .filter(SampleEventModel.sample_id == sample.id) .all() ) return samples @router.get("/pucks-samples", response_model=List[PuckSchema]) async def get_all_pucks_with_samples_and_events( active_pgroup: str, db: Session = Depends(get_db) ): logging.info( "Fetching all pucks with " "samples and events for active_pgroup: %s", active_pgroup, ) pucks = ( db.query(PuckModel) .join(PuckModel.samples) # Join samples related to the puck .join(PuckModel.dewar) # Join the dewar from the puck .join(SampleModel.events) # Join sample events .filter(DewarModel.pgroups == active_pgroup) # Filter by the dewar's group .options( joinedload(PuckModel.samples).joinedload(SampleModel.events), joinedload(PuckModel.dewar), ) .distinct() # Avoid duplicate puck rows if there are multiple events/samples .all() ) if not pucks: raise HTTPException( status_code=404, detail="No pucks found with" " sample events for the active pgroup", ) # Extract samples from each puck if needed filtered_samples = [] for puck in pucks: if puck.dewar and getattr(puck.dewar, "pgroups", None) == active_pgroup: for sample in puck.samples: filtered_samples.append(sample) # Depending on what your endpoint expects, # you may choose to return pucks or samples. # For now, we're returning the list of pucks. return pucks # Route to post a new sample event @router.post("/samples/{sample_id}/events", response_model=Sample) async def create_sample_event( sample_id: int, event: SampleEventCreate, 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") # Create the event sample_event = SampleEventModel( sample_id=sample_id, event_type=event.event_type, timestamp=datetime.now(), # Use the current timestamp ) db.add(sample_event) db.commit() db.refresh(sample_event) # Load events for the sample to be serialized in the response sample.events = ( db.query(SampleEventModel).filter(SampleEventModel.sample_id == sample_id).all() ) return sample # Return the sample, now including `mount_count` @router.post("/{sample_id}/upload-images", response_model=Image) async def upload_sample_image( sample_id: int, uploaded_file: UploadFile = File(...), comment: str = Form(None), db: Session = Depends(get_db), ): logging.info(f"Received file: {uploaded_file.filename}") # 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 the most recent sample event for the sample sample_event = ( db.query(SampleEventModel) .filter(SampleEventModel.sample_id == sample_id) .order_by(SampleEventModel.timestamp.desc()) # Sort by most recent event .first() ) if not sample_event: logging.debug(f"No events found for sample with id: {sample_id}") raise HTTPException( status_code=404, detail="No events found for the specified sample" ) # Log the found sample event for debugging logging.debug( f"Most recent event found for sample_id {sample_id}: " f"event_id={sample_event.id}, " f"type={sample_event.event_type}, " f"timestamp={sample_event.timestamp}" ) # Extract event type and timestamp for directory structure event_type = sample_event.event_type event_timestamp = sample_event.timestamp.strftime("%Y-%m-%d_%H-%M-%S") # Define Directory Structure pgroup = sample.puck.dewar.pgroups # adjust to sample or puck pgroups as needed today = datetime.now().strftime("%Y-%m-%d") dewar_name = ( sample.puck.dewar.dewar_name if sample.puck and sample.puck.dewar else "default_dewar" ) puck_name = sample.puck.puck_name if sample.puck else "default_puck" position = sample.position if sample.position else "default_position" # Add 'run/event' specific details to the folder structure base_dir = Path( f"images/{pgroup}/{today}/{dewar_name}/{puck_name}/" f"{position}/{event_type}_{event_timestamp}" ) base_dir.mkdir(parents=True, exist_ok=True) # 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.", ) file_path = base_dir / uploaded_file.filename logging.debug(f"Saving file {uploaded_file.filename} to {file_path}") 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.", ) # Create the payload from the Pydantic schema image_payload = ImageCreate( pgroup=pgroup, comment=comment, filepath=str(file_path), status="active", sample_id=sample_id, sample_event_id=int(sample_event.id), # Link to the most recent sample event ).dict() # Convert the payload to your mapped SQLAlchemy model instance new_image = ImageModel(**image_payload) db.add(new_image) db.commit() db.refresh(new_image) logging.info( f"Uploaded 1 file for sample {sample_id} and event {sample_event.id} and " f"added record {new_image.id} to the database." ) # Returning the mapped SQLAlchemy object, which will be converted to the # Pydantic response model. return new_image @router.get("/results", response_model=List[SampleResult]) async def get_sample_results(active_pgroup: str, db: Session = Depends(get_db)): # Query samples for the active pgroup using joins. samples = ( db.query(SampleModel) .join(SampleModel.puck) .join(PuckModel.dewar) .filter(DewarModel.pgroups == active_pgroup) .all() ) if not samples: raise HTTPException( status_code=404, detail="No samples found for the active pgroup" ) results = [] for sample in samples: # Query images associated with the sample, including the related event_type images = ( db.query(ImageModel) .options(joinedload(ImageModel.sample_event)) .filter(ImageModel.sample_id == sample.id) .all() ) # Query experiment parameters (which include beamline parameters) for the # sample. experiment_parameters = ( db.query(ExperimentParametersModel) .filter(ExperimentParametersModel.sample_id == sample.id) .all() ) print("Experiment Parameters for sample", sample.id, experiment_parameters) results.append( SampleResult( sample_id=sample.id, sample_name=sample.sample_name, puck_name=sample.puck.puck_name if sample.puck else None, dewar_name=sample.puck.dewar.dewar_name if (sample.puck and sample.puck.dewar) else None, images=[ ImageInfo( id=img.id, filepath=img.filepath, event_type=img.sample_event.event_type if img.sample_event else "Unknown", comment=img.comment, ) for img in images ], experiment_runs=[ ExperimentParametersRead( id=ex.id, run_number=ex.run_number, beamline_parameters=ex.beamline_parameters, sample_id=ex.sample_id, ) for ex in experiment_parameters ], ) ) return results @router.post( "/samples/{sample_id}/experiment_parameters", response_model=ExperimentParametersRead, ) def create_experiment_parameters_for_sample( sample_id: int, exp_params: ExperimentParametersCreate, db: Session = Depends(get_db), ): # Calculate the new run_number for the given sample. # This assumes that the run_number is computed as one plus the maximum # current value. last_exp = ( db.query(ExperimentParametersModel) .filter(ExperimentParametersModel.sample_id == sample_id) .order_by(ExperimentParametersModel.run_number.desc()) .first() ) new_run_number = last_exp.run_number + 1 if last_exp else 1 # Create a new ExperimentParameters record. The beamline_parameters are # stored as JSON. new_exp = ExperimentParametersModel( run_number=new_run_number, beamline_parameters=exp_params.beamline_parameters.dict() if exp_params.beamline_parameters else None, sample_id=sample_id, ) db.add(new_exp) db.commit() db.refresh(new_exp) # Create a "Collecting" sample event associated with the new experiment parameters new_event = SampleEventModel( sample_id=sample_id, event_type="Collecting", # The event type timestamp=datetime.now(), # Use current timestamp ) db.add(new_event) db.commit() return new_exp @router.post("/processing-results", response_model=ResultResponse) def create_result(payload: ResultCreate, db: Session = Depends(get_db)): # Check experiment existence experiment = ( db.query(ExperimentParametersModel) .filter(ExperimentParametersModel.id == payload.run_id) .first() ) if not experiment: raise HTTPException( status_code=404, detail="Experiment parameters (run) not found" ) result_entry = ResultsModel( sample_id=payload.sample_id, run_id=payload.run_id, result=payload.result.model_dump(), # Serialize entire result to JSON ) db.add(result_entry) db.commit() db.refresh(result_entry) return ResultResponse( id=result_entry.id, sample_id=result_entry.sample_id, run_id=result_entry.run_id, result=payload.result, # return original payload directly ) # @router.get("/results", response_model=list[ResultResponse]) # def get_results(sample_id: int, result_id: int, db: Session = Depends(get_db)): # query = db.query(Results) # # if sample_id: # query = query.filter(Results.sample_id == sample_id) # if result_id: # query = query.filter(Results.result_id == result_id) # # results = query.all() # if not results: # raise HTTPException(status_code=404, detail="No results found") # # return results