From ef981d1e45839b2109c54acd94404ff81d68d6dd Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:22:37 +0100 Subject: [PATCH] Refactor OpenAPI fetcher for improved clarity and robustness Reorganized and enhanced the OpenAPI fetch logic for better maintainability and error handling. Key updates include improved environment variable validation, more detailed error messages, streamlined configuration loading, and additional safety checks for file paths and directories. Added proper logging and ensured the process flow is easy to trace. --- backend/app/routers/sample.py | 55 ++++++++++++++++++++++++++++- backend/app/schemas.py | 10 ++++++ backend/main.py | 65 +++++++++++++++++------------------ pyproject.toml | 2 +- 4 files changed, 96 insertions(+), 36 deletions(-) diff --git a/backend/app/routers/sample.py b/backend/app/routers/sample.py index 2d24b5b..39db2eb 100644 --- a/backend/app/routers/sample.py +++ b/backend/app/routers/sample.py @@ -1,7 +1,13 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session from typing import List -from app.schemas import Puck as PuckSchema, Sample as SampleSchema +from datetime import datetime +from app.schemas import ( + Puck as PuckSchema, + Sample as SampleSchema, + SampleEventResponse, + SampleEventCreate, +) from app.models import ( Puck as PuckModel, Sample as SampleModel, @@ -45,3 +51,50 @@ async def get_all_pucks_with_samples_and_events(db: Session = Depends(get_db)): status_code=404, detail="No pucks found in the database" ) # More descriptive return pucks + + +# Route to post a new sample event +@router.post("/samples/{sample_id}/events", response_model=SampleEventResponse) +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) + + return ( + sample_event # Response will automatically use the SampleEventResponse schema + ) + + +# 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 last_event # Response will automatically use the SampleEventResponse schema diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 854bbce..a46f94d 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -83,6 +83,16 @@ class SampleEventCreate(BaseModel): event_type: str +class SampleEventResponse(BaseModel): + id: int + sample_id: int + event_type: str + timestamp: datetime + + class Config: + from_attributes = True + + class Results(BaseModel): # Define attributes for Results here pass diff --git a/backend/main.py b/backend/main.py index 903b536..3397eb6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -36,6 +36,28 @@ def get_project_metadata(): ) +def run_server(): + import uvicorn + + print(f"[INFO] Starting server in {environment} environment...") + print(f"[INFO] SSL Certificate Path: {cert_path}") + print(f"[INFO] SSL Key Path: {key_path}") + port = config.get("PORT", os.getenv("PORT")) + if not port: + print("[ERROR] No port defined in config or environment variables. Aborting!") + sys.exit(1) # Exit if no port is defined + port = int(port) + print(f"[INFO] Running on port {port}") + uvicorn.run( + app, + host="127.0.0.1" if environment in ["dev", "test"] else "0.0.0.0", + port=port, + log_level="debug", + ssl_keyfile=key_path, + ssl_certfile=cert_path, + ) + + # Get project metadata from pyproject.toml project_name, project_version = get_project_metadata() app = FastAPI( @@ -145,7 +167,6 @@ app.include_router(sample.router, prefix="/samples", tags=["samples"]) if __name__ == "__main__": import sys - import uvicorn from dotenv import load_dotenv from multiprocessing import Process from time import sleep @@ -169,52 +190,28 @@ if __name__ == "__main__": print("openapi.json generated successfully.") sys.exit(0) # Exit after generating the file - # Default behavior: Run the server + # Default behavior: Run the server based on the environment environment = os.getenv("ENVIRONMENT", "dev") port = int(os.getenv("PORT", 8000)) is_ci = os.getenv("CI", "false").lower() == "true" - def run_server(): - print(f"[INFO] Starting server in {environment} environment...") - print(f"[INFO] SSL Certificate Path: {cert_path}") - print(f"[INFO] SSL Key Path: {key_path}") - port = config.get("PORT", os.getenv("PORT")) - if not port: - print( - "[ERROR] No port defined in config or environment variables. Aborting!" - ) - sys.exit(1) # Exit if no port is defined - port = int(port) - print(f"[INFO] Running on port {port}") - uvicorn.run( - app, - host="127.0.0.1" if environment in ["dev", "test"] else "0.0.0.0", - port=port, - log_level="debug", - ssl_keyfile=key_path, - ssl_certfile=cert_path, - ) - - # Run in CI mode - # Generate or use SSL Key and Certificate - if environment in ["test", "dev", "ci"]: - ssl_dir = Path( - cert_path - ).parent # Ensure we work with the parent directory of the cert path - ssl_dir.mkdir( - parents=True, exist_ok=True - ) # Create the directory structure if it doesn't exist + if is_ci or environment == "test": + # Test or CI Mode: Run server process temporarily for test validation + ssl_dir = Path(cert_path).parent + ssl_dir.mkdir(parents=True, exist_ok=True) # Generate self-signed certs if missing if not Path(cert_path).exists() or not Path(key_path).exists(): print(f"[INFO] Generating self-signed SSL certificates at {ssl_dir}") ssl_heidi.generate_self_signed_cert(cert_path, key_path) + # Start the server as a subprocess, wait, then terminate server_process = Process(target=run_server) server_process.start() - sleep(5) # Wait 5 seconds to ensure the server starts without errors - server_process.terminate() # Terminate the server (test purposes) + sleep(5) # Wait for 5 seconds to verify the server is running + server_process.terminate() # Terminate the server process (for CI) server_process.join() # Ensure proper cleanup print("CI: Server started and terminated successfully for test validation.") else: + # Dev or Prod: Start the server as usual run_server() diff --git a/pyproject.toml b/pyproject.toml index c043e5c..c8832c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aareDB" -version = "0.1.0a5" +version = "0.1.0a6" description = "Backend for next gen sample management system" authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}] license = {text = "MIT"}