diff --git a/api/handlers.py b/api/handlers.py index 5eaf8fd..a41e0c5 100644 --- a/api/handlers.py +++ b/api/handlers.py @@ -2,10 +2,10 @@ from fastapi import APIRouter, Request, HTTPException, Depends from fastapi.responses import JSONResponse from typing import Dict, Any import config -from llm.models import LLMResponse +from llm.models import LLMResponse, JiraWebhookPayload from database.database import get_db_session # Removed Session import here from sqlalchemy.orm import Session # Added correct SQLAlchemy import -from database.crud import get_all_analysis_records, delete_all_analysis_records, get_analysis_by_id +from database.crud import get_all_analysis_records, delete_all_analysis_records, get_analysis_by_id, create_analysis_record router = APIRouter( prefix="/api", @@ -13,7 +13,7 @@ router = APIRouter( ) -@router.get("/requests") +@router.get("/request") async def get_analysis_records_endpoint(db: Session = Depends(get_db_session)): """Get analysis records""" try: @@ -28,6 +28,21 @@ async def get_analysis_records_endpoint(db: Session = Depends(get_db_session)): content={"error": str(e)} ) +@router.post("/request", status_code=201) +async def create_analysis_record_endpoint( + payload: JiraWebhookPayload, + db: Session = Depends(get_db_session) +): + """Create a new Jira analysis record""" + try: + db_record = create_analysis_record(db, payload) + return JSONResponse( + status_code=201, + content={"message": "Record created successfully", "record_id": db_record.id} + ) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to create record: {str(e)}") + @router.post("/test-llm") async def test_llm_endpoint(db: Session = Depends(get_db_session)): """Test endpoint for LLM integration""" @@ -50,7 +65,7 @@ async def test_llm_endpoint(db: Session = Depends(get_db_session)): } ) -@router.delete("/requests") +@router.delete("/request") async def delete_analysis_records_endpoint(db: Session = Depends(get_db_session)): """Delete analysis records""" try: @@ -63,7 +78,9 @@ async def delete_analysis_records_endpoint(db: Session = Depends(get_db_session) return JSONResponse( status_code=500, content={"error": str(e)}) -@router.get("/requests/{record_id}") + + +@router.get("/request/{record_id}") async def get_analysis_record_endpoint(record_id: int, db: Session = Depends(get_db_session)): """Get specific analysis record by ID""" record = get_analysis_by_id(db, record_id) diff --git a/database/crud.py b/database/crud.py index 1200c46..d317385 100644 --- a/database/crud.py +++ b/database/crud.py @@ -1,6 +1,6 @@ from loguru import logger from sqlalchemy.orm import Session -from datetime import datetime +from datetime import datetime, timezone import json from typing import Dict, Any, Optional @@ -11,12 +11,11 @@ def create_analysis_record(db: Session, payload: JiraWebhookPayload) -> JiraAnal """Creates a new Jira analysis record in the database.""" db_analysis = JiraAnalysis( issue_key=payload.issueKey, - project_key=payload.projectKey, status="pending", issue_summary=payload.summary, request_payload=payload.model_dump(), - created_at=datetime.utcnow(), - updated_at=datetime.utcnow() + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) db.add(db_analysis) db.commit() @@ -45,7 +44,7 @@ def update_analysis_record( db_analysis = db.query(JiraAnalysis).filter(JiraAnalysis.id == record_id).first() if db_analysis: db_analysis.status = status - db_analysis.updated_at = datetime.utcnow() + db_analysis.updated_at = datetime.now(timezone.utc) if analysis_result: db_analysis.analysis_result = analysis_result if error_message: diff --git a/database/models.py b/database/models.py index 2429c86..95f2be2 100644 --- a/database/models.py +++ b/database/models.py @@ -9,7 +9,6 @@ class JiraAnalysis(Base): id = Column(Integer, primary_key=True, index=True) issue_key = Column(String, index=True, nullable=False) - project_key = Column(String, index=True, nullable=False) status = Column(String, default="pending", nullable=False) # pending, processing, completed, failed issue_summary = Column(Text, nullable=False) request_payload = Column(JSON, nullable=False) # Store the original Jira webhook payload diff --git a/jira_analyses.db b/jira_analyses.db index 52d57ae..64627cb 100644 Binary files a/jira_analyses.db and b/jira_analyses.db differ diff --git a/jira_webhook_llm.py b/jira_webhook_llm.py index 32b5106..1edb34b 100644 --- a/jira_webhook_llm.py +++ b/jira_webhook_llm.py @@ -56,72 +56,57 @@ async def lifespan(app: FastAPI): executor = ThreadPoolExecutor(max_workers=settings.thread_pool_max_workers) app.state.executor = executor + # Flag to track if initialization succeeded + init_success = False + try: logger.info("Initializing application...") init_db() # Initialize the database - # Initialize event loop - loop = asyncio.get_running_loop() - logger.debug("Event loop initialized") - # Setup signal handlers + loop = asyncio.get_running_loop() for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, partial(handle_shutdown_signal, sig, loop)) logger.info("Signal handlers configured successfully") - + # Verify critical components if not hasattr(settings, 'langfuse_handler'): logger.error("Langfuse handler not found in settings") raise RuntimeError("Langfuse handler not initialized") - + logger.info("Application initialized successfully") - yield - - # Check shutdown flag before cleanup - loop = asyncio.get_running_loop() - if hasattr(loop, '_shutdown'): - logger.info("Shutdown initiated, starting cleanup...") + init_success = True except Exception as e: logger.critical(f"Application initialization failed: {str(e)}. Exiting.") - # Do not re-raise here, allow finally block to execute cleanup + # Don't re-raise to allow finally block to execute cleanup + + try: + # Yield control to the application + yield finally: - # Ensure async context for cleanup - async def perform_shutdown(): - try: - await execute_cleanup() - except Exception as e: - logger.error(f"Cleanup failed: {str(e)}") - finally: - loop.stop() - # Run within event loop - asyncio.get_event_loop().run_until_complete(perform_shutdown()) - - # Close langfuse with retry + # Cleanup logic runs after application finishes + if init_success: + # Check shutdown flag before cleanup + loop = asyncio.get_running_loop() + if hasattr(loop, '_shutdown'): + logger.info("Shutdown initiated, starting cleanup...") + + # Close langfuse with retry if hasattr(settings, 'langfuse_handler') and hasattr(settings.langfuse_handler, 'close'): - async def close_langfuse(): - try: - await asyncio.wait_for(settings.langfuse_handler.close(), timeout=5.0) - logger.info("Langfuse client closed successfully") - except asyncio.TimeoutError: - logger.warning("Timeout while closing Langfuse client") - except Exception as e: - logger.error(f"Error closing Langfuse client: {str(e)}") - cleanup_tasks.append(close_langfuse) - - # Wrap in async function and structure properly - async def execute_cleanup(): - try: - await asyncio.wait_for(asyncio.gather(*cleanup_tasks), timeout=10.0) - except asyncio.TimeoutError: - logger.warning("Timeout during cleanup sequence") - finally: - loop.stop() - # Wrap in try/except and ensure async context - # The following lines were causing syntax errors due to incorrect indentation and placement. - # The cleanup logic is already handled by `execute_cleanup` and `perform_shutdown`. - # Removing redundant and misplaced code. - - # Ensure proper top-level placement after async blocks + try: + await asyncio.wait_for(settings.langfuse_handler.close(), timeout=5.0) + logger.info("Langfuse client closed successfully") + except asyncio.TimeoutError: + logger.warning("Timeout while closing Langfuse client") + except Exception as e: + logger.error(f"Error closing Langfuse client: {str(e)}") + + # Execute any other cleanup tasks + if cleanup_tasks: + try: + await asyncio.gather(*cleanup_tasks) + except Exception as e: + logger.error(f"Error during additional cleanup tasks: {str(e)}") def create_app(): """Factory function to create FastAPI app instance""" configure_logging(log_level="DEBUG") @@ -177,7 +162,10 @@ def create_app(): return _app +from api.handlers import test_llm_endpoint + app = create_app() + async def process_jira_webhook_background(record_id: int, payload: JiraWebhookPayload): """ diff --git a/llm/models.py b/llm/models.py index f15d3cc..624d146 100644 --- a/llm/models.py +++ b/llm/models.py @@ -11,7 +11,6 @@ class JiraWebhookPayload(BaseModel): model_config = ConfigDict(alias_generator=lambda x: ''.join(word.capitalize() if i > 0 else word for i, word in enumerate(x.split('_'))), populate_by_name=True) issueKey: str - projectKey: Optional[str] = None # Added missing field summary: str description: Optional[str] = None comment: Optional[str] = None diff --git a/tests/conftest.py b/tests/conftest.py index b17cd59..3543b7f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from jira_webhook_llm import create_app, app # Assuming app is created via factory -from database.database import engine, Base # Add import +from jira_webhook_llm import create_app +from database.database import get_db_session, Base # Import get_db_session and Base from sqlalchemy.orm import sessionmaker import os @@ -33,7 +33,6 @@ def setup_db(monkeypatch): def mock_full_jira_payload(setup_db): mock_data = { "issueKey": "PROJ-123", - "projectKey": "PROJ", "summary": "Test Issue", "description": "Test Description", "comment": "Test Comment", @@ -45,26 +44,32 @@ def mock_full_jira_payload(setup_db): } return mock_data -@pytest.fixture -def test_client(setup_db): - # Ensure the database is set up before creating the app - test_engine = setup_db +@pytest.fixture(scope="function") +def test_client(setup_db, monkeypatch): + # Prevent signal handling in tests to avoid "set_wakeup_fd" error + monkeypatch.setattr("jira_webhook_llm.handle_shutdown_signal", lambda *args: None) - # Import and patch the database module to use test database - from database import database as db - db.engine = test_engine - db.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine) - - # Create fresh app instance with test DB - from jira_webhook_llm import create_app + # Create a test app instance app = create_app() - return TestClient(app) + + # Override the get_db_session dependency to use the test database + def override_get_db_session(): + with setup_db.connect() as connection: + with sessionmaker(autocommit=False, autoflush=False, bind=connection)() as session: + yield session + + app.dependency_overrides[get_db_session] = override_get_db_session + + with TestClient(app) as client: + yield client + + # Clean up dependency override + app.dependency_overrides.clear() @pytest.fixture def mock_jira_payload(): return { - "projectKey": "TEST-PROJECT", "issueKey": "TEST-123", "summary": "Test Issue", "description": "Test Description", diff --git a/tests/test_core.py b/tests/test_core.py index 518ba24..c4171bc 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2,6 +2,9 @@ import pytest from fastapi import HTTPException from jira_webhook_llm import app from llm.models import JiraWebhookPayload +from database.crud import create_analysis_record, get_analysis_by_id +from database.models import JiraAnalysis +from database.database import get_db def test_error_handling_middleware(test_client, mock_jira_payload): # Test 404 error handling @@ -12,13 +15,13 @@ def test_error_handling_middleware(test_client, mock_jira_payload): # Test validation error handling invalid_payload = mock_jira_payload.copy() invalid_payload.pop("issueKey") - response = test_client.post("/jira-webhook", json=invalid_payload) + response = test_client.post("/api/jira-webhook", json=invalid_payload) assert response.status_code == 422 assert "details" in response.json() def test_webhook_handler(setup_db, test_client, mock_full_jira_payload): # Test successful webhook handling with full payload - response = test_client.post("/jira-webhook", json=mock_full_jira_payload) + response = test_client.post("/api/jira-webhook", json=mock_full_jira_payload) assert response.status_code == 200 response_data = response.json() assert "status" in response_data @@ -34,14 +37,30 @@ def test_webhook_handler(setup_db, test_client, mock_full_jira_payload): assert record is not None assert record.issue_summary == mock_full_jira_payload["summary"] assert record.request_payload == mock_full_jira_payload - assert record.project_key == mock_full_jira_payload["projectKey"] def test_llm_test_endpoint(test_client): # Test LLM test endpoint - response = test_client.post("/test-llm") + response = test_client.post("/api/test-llm") assert response.status_code == 200 assert "response" in response.json() +def test_create_analysis_record_endpoint(setup_db, test_client, mock_full_jira_payload): + # Test successful creation of a new analysis record via API + response = test_client.post("/api/request", json=mock_full_jira_payload) + assert response.status_code == 201 + response_data = response.json() + assert "message" in response_data + assert response_data["message"] == "Record created successfully" + assert "record_id" in response_data + + # Verify the record exists in the database + with get_db() as db: + record = get_analysis_by_id(db, response_data["record_id"]) + assert record is not None + assert record.issue_key == mock_full_jira_payload["issueKey"] + assert record.issue_summary == mock_full_jira_payload["summary"] + assert record.request_payload == mock_full_jira_payload + @pytest.mark.asyncio async def test_retry_decorator(): # Test retry decorator functionality