feat: Add endpoint to create Jira analysis records and update existing endpoints for consistency
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run

This commit is contained in:
Ireneusz Bachanowicz 2025-07-18 00:57:28 +02:00
parent 935a8a49ae
commit 1de9f46517
8 changed files with 107 additions and 81 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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

Binary file not shown.

View File

@ -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):
"""

View File

@ -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

View File

@ -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",

View File

@ -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