Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pr-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ jobs:
return "D"
return "F"

def extract_issues(payload: dict) -> list[dict]:
def extract_issues(payload: dict) -> List[Dtr]:
issues = payload.get("issues")
if isinstance(issues, list):
return issues
Expand All @@ -175,7 +175,7 @@ jobs:
)
return f"- [{severity}] {line_text}: {message}"

results: list[dict] = []
results: List[Dtr] = []

for filename in changed_files:
file_path = Path(filename)
Expand Down
13 changes: 9 additions & 4 deletions backend/app/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

from dotenv import find_dotenv, load_dotenv
from typing import Optional

# Load .env from current directory or parent directories if present.
load_dotenv(find_dotenv(filename=".env", usecwd=True), override=False)
Expand Down Expand Up @@ -53,8 +54,7 @@ class Settings:
cache_enabled: bool = _bool_env("CACHE_ENABLED", True)
cache_ttl_seconds: int = _int_env("CACHE_TTL_SECONDS", 300)
cache_max_entries: int = _int_env("CACHE_MAX_ENTRIES", 100)
redis_url: str | None = os.getenv("REDIS_URL")
sentry_dsn: str | None = os.getenv("SENTRY_DSN")

sentry_traces_sample_rate: float = _float_env("SENTRY_TRACES_SAMPLE_RATE", 0.0)
enable_docs: bool = _bool_env("ENABLE_DOCS", False)
public_root_info: bool = _bool_env("PUBLIC_ROOT_INFO", False)
Expand All @@ -63,7 +63,12 @@ class Settings:
jwt_algorithm: str = os.getenv("JWT_ALGORITHM", "HS256")
access_token_minutes: int = _int_env("ACCESS_TOKEN_MINUTES", 720)
llm_enabled: bool = _bool_env("LLM_ENABLED", False)
llm_api_key: str | None = os.getenv("LLM_API_KEY")

# βœ… FIXED INDENTATION (ALL SAME LEVEL)
redis_url: Optional[str] = os.getenv("REDIS_URL")
sentry_dsn: Optional[str] = os.getenv("SENTRY_DSN")
llm_api_key: Optional[str] = os.getenv("LLM_API_KEY")

llm_base_url: str = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1")
llm_model: str = os.getenv("LLM_MODEL", "gpt-4o-mini")
llm_timeout_seconds: int = _int_env("LLM_TIMEOUT_SECONDS", 30)
Expand All @@ -82,4 +87,4 @@ class Settings:
)


settings = Settings()
settings = Settings()
11 changes: 6 additions & 5 deletions backend/app/routers/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import zipfile
from io import BytesIO
from pathlib import PurePosixPath
from typing import Optional, List, Dict

from fastapi import APIRouter, File, HTTPException, Query, Request, Response, UploadFile
from fastapi.responses import StreamingResponse
Expand Down Expand Up @@ -67,7 +68,7 @@
}


async def _stream_analysis(code: str, language_hint: str | None):
async def _stream_analysis(code: str, language_hint: Optional[str]):
"""Async generator that yields SSE chunks for each analysis section."""
t0 = time.perf_counter()
language = detect_language(code, language_hint)
Expand Down Expand Up @@ -149,7 +150,7 @@ def _is_ignored_member(name: str) -> bool:
return any(part.lower() in IGNORED_DIRS for part in path.parts)


def _add_skipped(skipped_files: list[str], reason: str) -> None:
def _add_skipped(skipped_files: List[str], reason: str) -> None:
if len(skipped_files) < MAX_SKIPPED_FILES:
skipped_files.append(reason)

Expand All @@ -174,7 +175,7 @@ async def analyze_stream(req: CodeRequest):
)
async def analyze_stream_get(
code: str = Query(..., min_length=1, max_length=50000, description="Source code to analyze"),
language: str | None = Query(None, description="Optional language hint"),
language: Optional[str] = Query(None, description="Optional language hint"),
):
if not code.strip():
raise HTTPException(status_code=400, detail="code must not be empty or whitespace")
Expand Down Expand Up @@ -261,8 +262,8 @@ async def analyze_zip(request: Request, file: UploadFile = File(...)):

t0 = time.perf_counter()

results: list[dict] = []
skipped_files: list[str] = []
results: List[Dtr] = []
skipped_files: List[str] = []
total_size = 0

with archive:
Expand Down
16 changes: 9 additions & 7 deletions backend/app/routers/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"""

from __future__ import annotations
from typing import List, Optional

from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field

Expand All @@ -14,16 +16,16 @@
class HistorySaveRequest(BaseModel):
code: str = Field(..., min_length=1, max_length=50000)
language: str
score: int | None = None
issue_count: int | None = None
score: Optional[int] = None
issue_count: Optional[int] = None


class HistoryEntry(BaseModel):
id: int
code_hash: str
language: str
score: int | None
issue_count: int | None
score: Optional[int]
issue_count: Optional[int]
timestamp: str
code_preview: str

Expand All @@ -39,15 +41,15 @@ async def save_history(body: HistorySaveRequest):
return {"id": entry_id, "status": "saved"}


@router.get("/", response_model=list[HistoryEntry])
@router.get("/", response_model=List[HistoryEntry])
async def get_history(
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
):
return await database.get_entries(limit=limit, offset=offset)


@router.get("/search", response_model=list[HistoryEntry])
@router.get("/search", response_model=List[HistoryEntry])
async def search_history(
q: str = Query(..., min_length=1),
limit: int = Query(20, ge=1, le=100),
Expand All @@ -60,4 +62,4 @@ async def delete_history(entry_id: int):
deleted = await database.delete_entry(entry_id)
if not deleted:
raise HTTPException(status_code=404, detail="History entry not found.")
return {"id": entry_id, "status": "deleted"}
return {"id": entry_id, "status": "deleted"}
27 changes: 18 additions & 9 deletions backend/app/routers/user_data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from typing import List

from ..database import get_db
from ..models import FavoriteResult, QueryHistory, User
Expand All @@ -15,9 +16,10 @@
router = APIRouter(prefix="/user", tags=["User Data"])


@router.get("/history", response_model=list[HistoryRecord])
@router.get("/history", response_model=List[HistoryRecord])
def list_history(
current_user: User = Depends(get_current_user), db: Session = Depends(get_db)
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
records = (
db.execute(
Expand Down Expand Up @@ -75,12 +77,15 @@ def delete_history(
):
record = db.execute(
select(QueryHistory).where(
QueryHistory.id == history_id, QueryHistory.user_id == current_user.id
QueryHistory.id == history_id,
QueryHistory.user_id == current_user.id
)
).scalar_one_or_none()

if record is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="History record not found"
status_code=status.HTTP_404_NOT_FOUND,
detail="History record not found"
)

db.execute(delete(QueryHistory).where(QueryHistory.id == history_id))
Expand All @@ -100,9 +105,10 @@ def clear_history(
return {"status": "cleared", "deleted": result.rowcount or 0}


@router.get("/favorites", response_model=list[FavoriteRecord])
@router.get("/favorites", response_model=List[FavoriteRecord])
def list_favorites(
current_user: User = Depends(get_current_user), db: Session = Depends(get_db)
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
records = (
db.execute(
Expand Down Expand Up @@ -163,12 +169,15 @@ def delete_favorite(
):
record = db.execute(
select(FavoriteResult).where(
FavoriteResult.id == favorite_id, FavoriteResult.user_id == current_user.id
FavoriteResult.id == favorite_id,
FavoriteResult.user_id == current_user.id
)
).scalar_one_or_none()

if record is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Favorite not found"
status_code=status.HTTP_404_NOT_FOUND,
detail="Favorite not found"
)

db.execute(delete(FavoriteResult).where(FavoriteResult.id == favorite_id))
Expand All @@ -185,4 +194,4 @@ def clear_favorites(
delete(FavoriteResult).where(FavoriteResult.user_id == current_user.id)
)
db.commit()
return {"status": "cleared", "deleted": result.rowcount or 0}
return {"status": "cleared", "deleted": result.rowcount or 0}
2 changes: 1 addition & 1 deletion backend/app/sanitize.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def sanitize_text_input(text: str) -> str:
return text


def sanitize_language_hint(language: str | None) -> str | None:
def sanitize_language_hint(language: Optional[str]) -> Optional[str]:
"""Normalize optional language hint from API clients."""
if language is None:
return None
Expand Down
4 changes: 2 additions & 2 deletions backend/app/schema_validators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Shared Pydantic field validators for user-supplied request data."""

from __future__ import annotations

from typing import Optional, List, Dict
from .sanitize import (
sanitize_code_input,
sanitize_language_hint,
Expand All @@ -15,5 +15,5 @@
validate_language_hint = sanitize_language_hint


def validate_chat_history(items: list[str]) -> list[str]:
def validate_chat_history(items: List[str]) -> List[str]:
return [sanitize_text_input(item) for item in items]
Loading