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
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
This commit is contained in:
parent
935a8a49ae
commit
1de9f46517
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
jira_analyses.db
BIN
jira_analyses.db
Binary file not shown.
@ -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):
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user