Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fluentmeet_test
REDIS_URL: redis://localhost:6379/1
run: |
pytest --cov=app --cov-fail-under=5 tests/
pytest --cov=app --cov-fail-under=60 tests/
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,23 @@ pytest tests/ -v --cov=app --cov-report=html --cov-report=term

---

## Linting & Formatting
- **Black**: Enforce consistent code formatting.
```bash
black .
```
- **isort**: Sort imports for readability.
```bash
isort .
```
- **ruff**: Linting for code quality and style.
```bash
ruff .
```
```bash
python -m ruff check .
```

## 🤝 Contributing

We welcome contributions! Please follow these steps:
Expand Down
18 changes: 16 additions & 2 deletions app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import pathlib
import tomllib

from pydantic_settings import BaseSettings, SettingsConfigDict


def get_version() -> str:
pyproject_path = pathlib.Path(__file__).parent.parent.parent / "pyproject.toml"
if pyproject_path.exists():
with pyproject_path.open("rb") as f:
data = tomllib.load(f)
return str(data.get("project", {}).get("version", "1.0.0"))
return "1.0.0"


class Settings(BaseSettings):
PROJECT_NAME: str = "FluentMeet"
VERSION: str = "1.0.0"
VERSION: str = get_version()
API_V1_STR: str = "/api/v1"

# Security
Expand Down Expand Up @@ -31,7 +43,9 @@ class Settings(BaseSettings):
VOICE_AI_API_KEY: str | None = None
OPENAI_API_KEY: str | None = None

model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)
model_config = SettingsConfigDict(
env_file=".env", case_sensitive=True, extra="ignore"
)


settings = Settings()
50 changes: 50 additions & 0 deletions app/core/error_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Any

from fastapi.responses import JSONResponse
from pydantic import BaseModel


class ErrorDetail(BaseModel):
field: str | None = None
message: str


class ErrorResponse(BaseModel):
status: str = "error"
code: str
message: str
details: list[ErrorDetail] = []


def create_error_response(
status_code: int,
code: str,
message: str,
details: list[dict[str, Any]] | None = None,
) -> JSONResponse:
"""
Helper to create a standardized JSON error response.
"""
error_details = []
if details:
for detail in details:
error_details.append(
ErrorDetail(
field=detail.get("field"),
message=detail.get("msg")
or detail.get("message")
or "Unknown error",
)
)

response_content = ErrorResponse(
status="error",
code=code,
message=message,
details=error_details,
)

return JSONResponse(
status_code=status_code,
content=response_content.model_dump(),
)
80 changes: 80 additions & 0 deletions app/core/exception_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import logging
from typing import Any

from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

from app.core.error_responses import create_error_response
from app.core.exceptions import FluentMeetException

logger = logging.getLogger(__name__)


async def fluentmeet_exception_handler(_request: Request, exc: Any) -> JSONResponse:
"""
Handler for all custom FluentMeetException exceptions.
"""
return create_error_response(
status_code=exc.status_code,
code=exc.code,
message=exc.message,
details=exc.details,
)


async def validation_exception_handler(_request: Request, exc: Any) -> JSONResponse:
"""
Handler for Pydantic validation errors (422 -> 400).
"""
details = []
for error in exc.errors():
details.append(
{
"field": ".".join(str(loc) for loc in error["loc"]),
"msg": error["msg"],
}
)

return create_error_response(
status_code=400,
code="VALIDATION_ERROR",
message="Request validation failed",
details=details,
)


async def http_exception_handler(_request: Request, exc: Any) -> JSONResponse:
Comment on lines +15 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unresolved Any usage vs imports in the reviewed file.
rg -n 'from typing import Any|\bAny\b' app/core/exception_handlers.py

Repository: Brints/FluentMeet

Length of output: 318


🏁 Script executed:

head -n 50 app/core/exception_handlers.py | cat -n

Repository: Brints/FluentMeet

Length of output: 1796


Fix undefined Any annotations to avoid module import/lint failures.

Lines 14, 26, and 47 use Any without importing it, which causes F821 pipeline failures and blocks CI.

🔧 Proposed fix
 import logging
 
 from fastapi import FastAPI, Request
-async def fluentmeet_exception_handler(_request: Request, exc: Any) -> JSONResponse:
+async def fluentmeet_exception_handler(
+    _request: Request, exc: FluentMeetException
+) -> JSONResponse:
-async def validation_exception_handler(_request: Request, exc: Any) -> JSONResponse:
+async def validation_exception_handler(
+    _request: Request, exc: RequestValidationError
+) -> JSONResponse:
-async def http_exception_handler(_request: Request, exc: Any) -> JSONResponse:
+async def http_exception_handler(
+    _request: Request, exc: StarletteHTTPException
+) -> JSONResponse:

Replace Any with the concrete exception types that are already imported (FluentMeetException, RequestValidationError, StarletteHTTPException). This provides stronger typing and is already available in the module.

🧰 Tools
🪛 GitHub Actions: Code Quality

[error] 14-14: F821 Undefined name 'Any' in type annotation.


[error] 26-26: F821 Undefined name 'Any' in type annotation.


[error] 47-47: F821 Undefined name 'Any' in type annotation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/core/exception_handlers.py` around lines 14 - 47, Change the broad Any
annotations on the exception handler signatures to the concrete exception types
already imported: update fluentmeet_exception_handler to accept
FluentMeetException, validation_exception_handler to accept
RequestValidationError, and http_exception_handler to accept
StarletteHTTPException; this requires editing the function parameters in the
definitions (fluentmeet_exception_handler, validation_exception_handler,
http_exception_handler) to use those specific types so linting/import checks
pass and typing is stronger.

"""
Handler for Starlette/FastAPI HTTP exceptions.
"""
return create_error_response(
status_code=exc.status_code,
code=getattr(exc, "code", "HTTP_ERROR"),
message=exc.detail,
)


async def unhandled_exception_handler(
_request: Request, exc: Exception
) -> JSONResponse:
"""
Handler for all other unhandled exceptions (500).
"""
logger.exception("Unhandled exception occurred: %s", str(exc))
return create_error_response(
status_code=500,
code="INTERNAL_SERVER_ERROR",
message="An unexpected server error occurred",
)


def register_exception_handlers(app: FastAPI) -> None:
"""
Register all custom exception handlers to the FastAPI app.
"""
app.add_exception_handler(FluentMeetException, fluentmeet_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(StarletteHTTPException, http_exception_handler)
app.add_exception_handler(Exception, unhandled_exception_handler)
80 changes: 80 additions & 0 deletions app/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Any


class FluentMeetException(Exception):
"""
Base exception for all FluentMeet API errors.
"""

def __init__(
self,
status_code: int = 500,
code: str = "INTERNAL_SERVER_ERROR",
message: str = "An unexpected error occurred",
details: list[dict[str, Any]] | None = None,
) -> None:
self.status_code = status_code
self.code = code
self.message = message
self.details = details or []
super().__init__(self.message)


class BadRequestException(FluentMeetException):
def __init__(
self,
message: str = "Bad Request",
code: str = "BAD_REQUEST",
details: list[dict[str, Any]] | None = None,
) -> None:
super().__init__(400, code, message, details)


class UnauthorizedException(FluentMeetException):
def __init__(
self,
message: str = "Unauthorized",
code: str = "UNAUTHORIZED",
details: list[dict[str, Any]] | None = None,
) -> None:
super().__init__(401, code, message, details)


class ForbiddenException(FluentMeetException):
def __init__(
self,
message: str = "Forbidden",
code: str = "FORBIDDEN",
details: list[dict[str, Any]] | None = None,
) -> None:
super().__init__(403, code, message, details)


class NotFoundException(FluentMeetException):
def __init__(
self,
message: str = "Not Found",
code: str = "NOT_FOUND",
details: list[dict[str, Any]] | None = None,
) -> None:
super().__init__(404, code, message, details)


class ConflictException(FluentMeetException):
def __init__(
self,
message: str = "Conflict",
code: str = "CONFLICT",
details: list[dict[str, Any]] | None = None,
) -> None:
super().__init__(409, code, message, details)


class InternalServerException(FluentMeetException):
def __init__(
self,
message: str = "Internal Server Error",
code: str = "INTERNAL_SERVER_ERROR",
details: list[dict[str, Any]] | None = None,
) -> None:
super().__init__(500, code, message, details)
11 changes: 8 additions & 3 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.core.config import settings
from app.core.exception_handlers import register_exception_handlers

app = FastAPI(
title="FluentMeet API",
title=settings.PROJECT_NAME,
description="Real-time voice translation video conferencing platform API",
version="1.1.0",
version=settings.VERSION,
)

# Set all CORS enabled origins
Expand All @@ -16,10 +19,12 @@
allow_headers=["*"],
)

register_exception_handlers(app)


@app.get("/health", tags=["health"])
async def health_check() -> dict[str, str]:
return {"status": "ok", "version": "1.1.0"}
return {"status": "ok", "version": settings.VERSION}


if __name__ == "__main__":
Expand Down
File renamed without changes.
33 changes: 0 additions & 33 deletions linting_issue.md

This file was deleted.

Loading
Loading