chore(lint): fix all ruff errors — 0 warnings remaining

- B904 (55): add `from err` / `from None` to raise-in-except across 13 files
- F821 (1): add missing HTTPException import in routes_language_qc.py
- F841 (7): remove unused variable assignments (current_user, job_title, tts_provider, etc.)
- W293 (13): strip trailing whitespace from blank lines
- C416 (4): rewrite unnecessary dict comprehensions as dict()
- C401 (1): rewrite unnecessary generator as set comprehension
- E701 (4): split multi-statement lines in cost_tracker.py
- E741 (1): rename ambiguous `l` to `lang` in cloud_run_dispatch.py
- B007 (4): prefix unused loop variables with _ in tts.py, video_renderer.py
- I001 (1): sort imports in tasks/__init__.py (move stdlib to top)
- E402 (3): move threading/time/signals imports to top of tasks/__init__.py
- UP042 (9): replace (str, Enum) with StrEnum in all model/schema enums

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-13 17:13:08 +01:00
parent 16000a8bd9
commit ca312d48fa
33 changed files with 120 additions and 123 deletions

View file

@ -253,7 +253,7 @@ async def update_user(
action = AuditAction.USER_ROLE_CHANGE if user_update.role else AuditAction.USER_UPDATE
await log_user_management(
action, user_id, current_user, request,
details={k: v for k, v in user_update.dict(exclude_none=True).items()},
details=dict(user_update.dict(exclude_none=True).items()),
)
return UserResponse(
@ -439,7 +439,7 @@ async def detailed_health_check(
try:
from ...services.gcs import gcs_service
# Simple check to see if bucket is accessible
bucket_exists = await gcs_service.file_exists("health_check_dummy") # This will return False but won't error if bucket accessible
await gcs_service.file_exists("health_check_dummy") # This will return False but won't error if bucket accessible
health_status["components"]["gcs"] = {"status": "healthy"}
except Exception as e:
health_status["components"]["gcs"] = {"status": "unhealthy", "error": str(e)}

View file

@ -143,13 +143,13 @@ async def microsoft_login(
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Microsoft authentication failed: {str(e)}",
)
) from None
except MicrosoftAuthError as e:
await log_auth_failure("microsoft-sso", request, f"MS auth service error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Microsoft authentication service error",
)
) from None
# Look up by Microsoft-derived ID first — handles email casing changes across logins
ms_user_id = f"ms-{user_info.sub[:20]}"
@ -287,7 +287,7 @@ async def refresh_token(
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
)
) from None
@router.post("/logout", response_model=LogoutResponse)

View file

@ -162,7 +162,7 @@ async def update_client(
db: AsyncIOMotorDatabase = Depends(get_database),
):
await _get_client_or_404(client_id, db)
update: dict = {k: v for k, v in body.model_dump(exclude_none=True).items()}
update: dict = dict(body.model_dump(exclude_none=True).items())
if not update:
raise HTTPException(status_code=422, detail="No fields to update")
if "slug" in update and await db.clients.find_one({"slug": update["slug"], "_id": {"$ne": client_id}}):
@ -296,7 +296,7 @@ async def update_team(
await _get_client_or_404(client_id, db)
await _assert_pm_or_admin(current_user, client_id, db)
await _get_team_or_404(team_id, client_id, db)
update = {k: v for k, v in body.model_dump(exclude_none=True).items()}
update = dict(body.model_dump(exclude_none=True).items())
if not update:
raise HTTPException(status_code=422, detail="No fields to update")
update["updated_at"] = _now()
@ -440,7 +440,7 @@ async def update_project(
await _get_client_or_404(client_id, db)
await _assert_pm_or_admin(current_user, client_id, db)
await _get_project_or_404(project_id, client_id, db)
update = {k: v for k, v in body.model_dump(exclude_none=True).items()}
update = dict(body.model_dump(exclude_none=True).items())
if not update:
raise HTTPException(status_code=422, detail="No fields to update")
update["updated_at"] = _now()

View file

@ -62,4 +62,4 @@ async def get_signed_upload_url(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to generate signed upload URL: {str(e)}"
)
) from None

View file

@ -133,7 +133,7 @@ async def complete_chunked_upload(
outputs_data = json.loads(json.dumps(payload.requested_outputs))
outputs = RequestedOutputs(**outputs_data)
except Exception:
raise HTTPException(status_code=400, detail="Invalid requested_outputs format")
raise HTTPException(status_code=400, detail="Invalid requested_outputs format") from None
organization_id: str | None = None
brief_doc = None
@ -196,7 +196,7 @@ async def complete_chunked_upload(
logger.info("Dispatched ingest task for chunked-upload job %s", payload.job_id)
except Exception as e:
logger.error("Failed to dispatch ingest task for job %s: %s", payload.job_id, e)
raise HTTPException(status_code=500, detail=f"Failed to start processing: {e}")
raise HTTPException(status_code=500, detail=f"Failed to start processing: {e}") from None
await log_job_action(
AuditAction.JOB_CREATE, payload.job_id, current_user, request,
@ -246,7 +246,7 @@ async def create_job(
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid requested_outputs format"
)
) from None
# Resolve brief if provided — overrides some fields and sets organization_id
brief_doc = None
@ -330,7 +330,7 @@ async def create_job(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to start processing: {e}",
)
) from None
await log_job_action(
AuditAction.JOB_CREATE, job_id, current_user, request,
@ -774,7 +774,6 @@ async def get_job(
db: AsyncIOMotorDatabase = Depends(get_database),
):
job_doc = await get_job_or_403(job_id, ctx, db)
current_user = ctx.user
# Check task status if task_id exists
task_id = job_doc.get("task_id")
@ -1586,7 +1585,7 @@ async def update_job_vtt_content(
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"You are not assigned to language '{target_language}'"
)
) from None
outputs = job_doc.get("outputs", {})
lang_output = outputs.get(target_language, {})
@ -2024,7 +2023,7 @@ async def adjust_vtt_timing(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to adjust captions timing"
)
) from None
# Adjust audio description VTT if requested and exists
if request.adjust_audio_description and "ad_vtt_gcs" in outputs:
@ -2055,7 +2054,7 @@ async def adjust_vtt_timing(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to adjust audio description timing"
)
) from None
if not update_operations:
raise HTTPException(
@ -2196,7 +2195,7 @@ async def delete_job(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete job: {str(e)}"
)
) from None
async def _delete_job_gcs_assets(job_id: str, job_doc: dict):
@ -2322,7 +2321,7 @@ async def retry_tts(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to start TTS retry"
)
) from None
return JobResponse(
id=str(result["_id"]),
@ -2460,7 +2459,7 @@ async def retry_job(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to start retry task",
)
) from None
return JobResponse(
id=str(result["_id"]),
@ -2930,7 +2929,7 @@ async def trigger_accessible_video_rerender(
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"You are not assigned to language '{language}'"
)
) from None
# Get edit state
lang_output = job_doc.get("outputs", {}).get(language)
@ -2950,7 +2949,6 @@ async def trigger_accessible_video_rerender(
]
# Update job status to RENDERING_QC — conditional to prevent concurrent render races
job_title = job_doc.get("title", "Untitled Job")
transition_result = await db.jobs.update_one(
{"_id": job_id, "status": JobStatus.PENDING_QC.value}, # Only transition from PENDING_QC
{

View file

@ -2,7 +2,7 @@
from datetime import datetime
from fastapi import APIRouter, Depends, Query, Request
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from motor.motor_asyncio import AsyncIOMotorDatabase
from pydantic import BaseModel, Field
@ -205,7 +205,6 @@ async def bulk_assign_languages(
"""Assign one linguist (and optionally one reviewer) to multiple languages in one call."""
job_doc = await db["jobs"].find_one({"_id": job_id})
if not job_doc:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Job not found")
available = list((job_doc.get("outputs") or {}).keys())
@ -352,10 +351,6 @@ async def mark_cue_reviewed(
if not job_doc:
raise HTTPException(status_code=404, detail="Job not found")
update: dict = {
f"language_qc.{lang}.reviewed_cues": 1, # will use $inc below
"updated_at": datetime.utcnow(),
}
inc_op: dict = {f"language_qc.{lang}.reviewed_cues": 1}
set_op: dict = {"updated_at": datetime.utcnow()}

View file

@ -129,7 +129,7 @@ async def restore_vtt_version(
raise HTTPException(
status_code=500,
detail=f"Version snapshot created (v{new_ver.version}) but live file update failed: {exc}",
)
) from None
# Update the GCS URI pointer in the job document
gcs_uri_key = "captions_vtt_gcs" if kind == "captions" else "ad_vtt_gcs"

View file

@ -58,4 +58,4 @@ def decode_token(token: str) -> dict[str, Any]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
) from None

View file

@ -25,7 +25,7 @@ class RateLimiter:
) -> tuple[bool, dict[str, int]]:
"""
Check if request is allowed under rate limit.
Returns:
Tuple of (is_allowed, rate_limit_info)
"""

View file

@ -125,9 +125,9 @@ class RequestValidator:
subtitle_extensions = {'vtt', 'srt', 'txt'}
if expected_type == "video" and ext not in video_extensions:
raise ValidationError(f"Invalid video file extension: {ext}")
raise ValidationError(f"Invalid video file extension: {ext}") from None
elif expected_type == "subtitle" and ext not in subtitle_extensions:
raise ValidationError(f"Invalid subtitle file extension: {ext}")
raise ValidationError(f"Invalid subtitle file extension: {ext}") from None
return
if expected_type == "video" and detected_type not in self.allowed_video_types:
@ -186,7 +186,7 @@ class RequestValidator:
return payload
except json.JSONDecodeError as e:
raise ValidationError(f"Invalid JSON: {e}")
raise ValidationError(f"Invalid JSON: {e}") from e
def _validate_json_values(self, obj: Any, path: str = "root") -> None:
"""Recursively validate JSON values."""

View file

@ -141,10 +141,10 @@ class MigrationManager:
async def migrate_up(self, target_version: str | None = None) -> list[str]:
"""
Apply migrations up to the target version.
Args:
target_version: Version to migrate to. If None, applies all pending migrations.
Returns:
List of applied migration versions.
"""
@ -189,10 +189,10 @@ class MigrationManager:
async def migrate_down(self, target_version: str) -> list[str]:
"""
Rollback migrations down to the target version.
Args:
target_version: Version to rollback to.
Returns:
List of rolled back migration versions.
"""

View file

@ -1,7 +1,7 @@
"""Entry point for running migrations: python -m app.migrations.run"""
import asyncio
from app.core.database import connect_to_mongo, close_mongo_connection
from app.core.database import close_mongo_connection, connect_to_mongo
from app.migrations.migrator import MigrationManager

View file

@ -1,7 +1,7 @@
"""Audit log model for tracking sensitive operations."""
from datetime import datetime
from enum import Enum
from enum import StrEnum
from typing import Any
from bson import ObjectId
@ -10,7 +10,7 @@ from pydantic import BaseModel, Field
from .user import PyObjectId
class AuditAction(str, Enum):
class AuditAction(StrEnum):
"""Enumeration of auditable actions."""
# Authentication actions
@ -84,7 +84,7 @@ class AuditAction(str, Enum):
SUSPICIOUS_ACTIVITY = "security.suspicious.activity"
class AuditLogSeverity(str, Enum):
class AuditLogSeverity(StrEnum):
"""Severity levels for audit events."""
INFO = "info" # Normal operations

View file

@ -1,5 +1,5 @@
from datetime import datetime
from enum import Enum
from enum import StrEnum
from typing import Any, Literal
from pydantic import BaseModel, Field, constr
@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, constr
FailureStep = Literal["ingestion", "ai_processing", "translation", "tts", "render"]
class JobStatus(str, Enum):
class JobStatus(StrEnum):
CREATED = "created"
INGESTING = "ingesting"
AI_PROCESSING = "ai_processing"
@ -158,7 +158,7 @@ class Review(BaseModel):
# ── Per-language QC ───────────────────────────────────────────────────────────
class LanguageQCStatus(str, Enum):
class LanguageQCStatus(StrEnum):
PENDING = "pending"
IN_PROGRESS = "in_progress" # linguist is working
PENDING_REVIEW = "pending_review" # linguist submitted, awaiting reviewer

View file

@ -1,13 +1,13 @@
"""Job Brief model — pre-approved work order submitted before job creation."""
from datetime import datetime
from enum import Enum
from enum import StrEnum
from pydantic import BaseModel, Field
from .job import RequestedOutputs
class BriefStatus(str, Enum):
class BriefStatus(StrEnum):
DRAFT = "draft"
SUBMITTED = "submitted"
APPROVED = "approved"

View file

@ -1,10 +1,10 @@
from datetime import datetime
from enum import Enum
from enum import StrEnum
from pydantic import BaseModel
class OrgRole(str, Enum):
class OrgRole(StrEnum):
OWNER = "owner"
ADMIN = "admin"
MANAGER = "manager"

View file

@ -1,5 +1,5 @@
from datetime import datetime
from enum import Enum
from enum import StrEnum
from typing import Annotated
from bson import ObjectId
@ -18,7 +18,7 @@ def validate_object_id(v) -> str:
PyObjectId = Annotated[str, BeforeValidator(validate_object_id)]
class UserRole(str, Enum):
class UserRole(StrEnum):
CLIENT = "client"
REVIEWER = "reviewer"
LINGUIST = "linguist"
@ -27,7 +27,7 @@ class UserRole(str, Enum):
ADMIN = "admin"
class AuthProvider(str, Enum):
class AuthProvider(StrEnum):
LOCAL = "local"
MICROSOFT = "microsoft"

View file

@ -1,11 +1,11 @@
"""Schemas for accessible video generation with embedded audio descriptions."""
from enum import Enum
from enum import StrEnum
from pydantic import BaseModel, Field
class AccessibleVideoMethod(str, Enum):
class AccessibleVideoMethod(StrEnum):
"""Method used for integrating audio descriptions into video."""
OVERLAY = "overlay"
PAUSE_INSERT = "pause_insert"

View file

@ -51,7 +51,7 @@ class AuditLogger:
) -> str:
"""
Log an audit event.
Returns:
The ID of the created audit log entry.
"""

View file

@ -82,7 +82,7 @@ def _celery_fallback(task: str, job_id: str, **extra_args) -> str:
from ..tasks.translate_and_synthesize import translate_and_synthesize_task
_langs = extra_args.get("languages")
if isinstance(_langs, str):
_langs = [l for l in _langs.split(",") if l]
_langs = [lang for lang in _langs.split(",") if lang]
translate_and_synthesize_task.delay(job_id, languages=_langs or None)
elif task == "render":
from ..tasks.render_accessible_video import render_accessible_video_task

View file

@ -75,8 +75,10 @@ def record(
if chars is not None:
units["char"] = chars
else:
if input_tokens: units["token_input"] = input_tokens
if output_tokens: units["token_output"] = output_tokens
if input_tokens:
units["token_input"] = input_tokens
if output_tokens:
units["token_output"] = output_tokens
payload: dict = {
"source_app": settings.cost_tracker_source_app,
@ -87,8 +89,10 @@ def record(
"latency_ms": latency_ms,
"status": status,
}
if project_id: payload["project_external_id"] = project_id
if job_external_id: payload["job_external_id"] = job_external_id
if project_id:
payload["project_external_id"] = project_id
if job_external_id:
payload["job_external_id"] = job_external_id
httpx.post(
f"{settings.cost_tracker_base_url}/usage/record",

View file

@ -273,7 +273,7 @@ async def run_ffmpeg(request: RunFFmpegRequest):
raise
except Exception as e:
logger.error(f"FFmpeg operation failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None
@app.post("/probe", response_model=ProbeResponse)
@ -328,7 +328,7 @@ async def probe_video(request: ProbeRequest):
raise
except Exception as e:
logger.error(f"Probe failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None
@app.post("/encode-segment", response_model=RunFFmpegResponse)
@ -380,7 +380,7 @@ async def encode_segment(request: EncodeSegmentRequest):
raise
except Exception as e:
logger.error(f"Encode segment failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None
@app.post("/extract-frame", response_model=RunFFmpegResponse)
@ -425,7 +425,7 @@ async def extract_frame(request: ExtractFrameRequest):
raise
except Exception as e:
logger.error(f"Extract frame failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None
@app.post("/create-freeze-segment", response_model=RunFFmpegResponse)
@ -480,7 +480,7 @@ async def create_freeze_segment(request: CreateFreezeSegmentRequest):
raise
except Exception as e:
logger.error(f"Create freeze segment failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None
@app.post("/concatenate", response_model=RunFFmpegResponse)
@ -534,4 +534,4 @@ async def concatenate_segments(request: ConcatenateRequest):
raise
except Exception as e:
logger.error(f"Concatenate failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None

View file

@ -55,7 +55,7 @@ class GCSService:
return await loop.run_in_executor(self.executor, _upload)
except Exception as e:
logger.error(f"Failed to upload file to GCS: {e}")
raise HTTPException(status_code=500, detail="File upload failed")
raise HTTPException(status_code=500, detail="File upload failed") from None
async def upload_text_to_gcs(
self,
@ -76,7 +76,7 @@ class GCSService:
return await loop.run_in_executor(self.executor, _upload)
except Exception as e:
logger.error(f"Failed to upload text to GCS: {e}")
raise HTTPException(status_code=500, detail="Text upload failed")
raise HTTPException(status_code=500, detail="Text upload failed") from None
async def get_signed_url(
self,
@ -104,10 +104,10 @@ class GCSService:
try:
return await loop.run_in_executor(self.executor, _get_signed_url)
except NotFound:
raise HTTPException(status_code=404, detail="File not found")
raise HTTPException(status_code=404, detail="File not found") from None
except Exception as e:
logger.error(f"Failed to generate signed URL: {e}")
raise HTTPException(status_code=500, detail="Failed to generate download URL")
raise HTTPException(status_code=500, detail="Failed to generate download URL") from None
async def create_resumable_upload_session(self, blob_path: str, content_type: str) -> str:
"""Create a GCS resumable upload session and return the session URI."""
@ -123,7 +123,7 @@ class GCSService:
return await loop.run_in_executor(self.executor, _create)
except Exception as e:
logger.error(f"Failed to create resumable upload session: {e}")
raise HTTPException(status_code=500, detail="Failed to initiate upload session")
raise HTTPException(status_code=500, detail="Failed to initiate upload session") from None
async def delete_file(self, blob_path: str) -> bool:
"""Delete a file from GCS"""
@ -139,7 +139,7 @@ class GCSService:
return False
except Exception as e:
logger.error(f"Failed to delete file from GCS: {e}")
raise HTTPException(status_code=500, detail="File deletion failed")
raise HTTPException(status_code=500, detail="File deletion failed") from None
async def file_exists(self, blob_path: str) -> bool:
"""Check if a file exists in GCS"""

View file

@ -358,7 +358,7 @@ Fix the JSON and return it:
except Exception as e:
logger.error(f"Self-heal attempt failed: {e}")
raise ValueError("Failed to get valid JSON from Gemini after self-heal attempt")
raise ValueError("Failed to get valid JSON from Gemini after self-heal attempt") from e
async def extract_accessibility_targeted(
self,
@ -567,7 +567,7 @@ Fix the JSON and return it:
except Exception as e:
logger.error(f"Self-heal attempt failed for {target_language}: {e}")
raise ValueError(f"Failed to get valid JSON from Gemini targeted extraction for {target_language}")
raise ValueError(f"Failed to get valid JSON from Gemini targeted extraction for {target_language}") from e
def _attempt_json_fix(self, json_text: str) -> dict[str, Any] | None:
"""Attempt to fix common JSON syntax issues"""
@ -788,7 +788,7 @@ Fix the JSON and return it:
except Exception as e:
logger.error(f"Self-heal attempt for accessible video analysis failed: {e}")
raise ValueError("Failed to get valid JSON from accessible video analysis after self-heal")
raise ValueError("Failed to get valid JSON from accessible video analysis after self-heal") from e
async def transcreate_content(
self,
@ -845,7 +845,7 @@ JSON:
except json.JSONDecodeError as e:
logger.error(f"Failed to parse transcreation JSON response: {e}")
raise ValueError("Invalid JSON response from transcreation")
raise ValueError("Invalid JSON response from transcreation") from e
except Exception as e:
logger.error(f"Transcreation failed: {e}")
raise

View file

@ -62,7 +62,7 @@ class MicrosoftAuthService:
return response.json()
except httpx.HTTPError as e:
logger.error(f"Failed to fetch OpenID configuration: {e}")
raise MicrosoftAuthError("Failed to fetch Microsoft authentication configuration")
raise MicrosoftAuthError("Failed to fetch Microsoft authentication configuration") from e
async def _get_jwks(self, force_refresh: bool = False) -> dict:
"""Fetch JSON Web Key Set (JWKS) from Microsoft.
@ -97,7 +97,7 @@ class MicrosoftAuthService:
except httpx.HTTPError as e:
logger.error(f"Failed to fetch JWKS: {e}")
raise MicrosoftAuthError("Failed to fetch Microsoft public keys")
raise MicrosoftAuthError("Failed to fetch Microsoft public keys") from e
async def validate_token(self, id_token: str) -> MicrosoftUserInfo:
"""Validate Microsoft ID token and extract user information.
@ -145,7 +145,7 @@ class MicrosoftAuthService:
issuer=f"https://login.microsoftonline.com/{self.tenant_id}/v2.0"
)
except JWTError as e:
raise MicrosoftTokenValidationError(f"Token validation failed: {str(e)}")
raise MicrosoftTokenValidationError(f"Token validation failed: {str(e)}") from e
email = payload.get('email') or payload.get('preferred_username')
if not email:
@ -176,12 +176,12 @@ class MicrosoftAuthService:
except JWKError as e:
logger.error(f"JWK error during token validation: {e}")
raise MicrosoftTokenValidationError(f"Key processing error: {str(e)}")
raise MicrosoftTokenValidationError(f"Key processing error: {str(e)}") from e
except Exception as e:
if isinstance(e, (MicrosoftAuthError, MicrosoftTokenValidationError)):
raise
logger.error(f"Unexpected error during token validation: {e}")
raise MicrosoftTokenValidationError(f"Token validation failed: {str(e)}")
raise MicrosoftTokenValidationError(f"Token validation failed: {str(e)}") from e
# Singleton instance

View file

@ -36,7 +36,7 @@ class SecretsManager:
logger.info("Secret Manager client initialized")
except Exception as e:
logger.error(f"Failed to initialize Secret Manager client: {e}")
raise SecretManagerError(f"Failed to initialize Secret Manager: {e}")
raise SecretManagerError(f"Failed to initialize Secret Manager: {e}") from e
return self.client
@ -44,14 +44,14 @@ class SecretsManager:
async def get_secret(self, secret_name: str, version: str = "latest") -> str:
"""
Retrieve a secret from Google Cloud Secret Manager.
Args:
secret_name: Name of the secret
version: Version of the secret (default: "latest")
Returns:
The secret value as a string
Raises:
SecretManagerError: If secret cannot be retrieved
"""
@ -89,26 +89,26 @@ class SecretsManager:
except gcp_exceptions.NotFound:
error_msg = f"Secret not found: {secret_name}"
logger.error(error_msg)
raise SecretManagerError(error_msg)
raise SecretManagerError(error_msg) from None
except gcp_exceptions.PermissionDenied:
error_msg = f"Permission denied accessing secret: {secret_name}"
logger.error(error_msg)
raise SecretManagerError(error_msg)
raise SecretManagerError(error_msg) from None
except Exception as e:
error_msg = f"Failed to retrieve secret {secret_name}: {e}"
logger.error(error_msg)
raise SecretManagerError(error_msg)
raise SecretManagerError(error_msg) from e
@trace_async_operation("secrets_manager.get_secrets_batch")
async def get_secrets_batch(self, secret_names: list[str]) -> dict[str, str]:
"""
Retrieve multiple secrets efficiently.
Args:
secret_names: List of secret names to retrieve
Returns:
Dictionary mapping secret names to their values
"""
@ -137,12 +137,12 @@ class SecretsManager:
async def create_secret(self, secret_name: str, secret_value: str, labels: dict[str, str] | None = None) -> str:
"""
Create a new secret in Secret Manager.
Args:
secret_name: Name of the secret
secret_value: Value to store
labels: Optional labels for the secret
Returns:
The full secret resource name
"""
@ -186,12 +186,12 @@ class SecretsManager:
except gcp_exceptions.AlreadyExists:
error_msg = f"Secret already exists: {secret_name}"
logger.error(error_msg)
raise SecretManagerError(error_msg)
raise SecretManagerError(error_msg) from None
except Exception as e:
error_msg = f"Failed to create secret {secret_name}: {e}"
logger.error(error_msg)
raise SecretManagerError(error_msg)
raise SecretManagerError(error_msg) from e
def clear_cache(self) -> None:
"""Clear the secrets cache."""
@ -217,7 +217,7 @@ async def get_database_url() -> str:
# Fallback to environment variable
url = os.getenv("MONGODB_URL")
if not url:
raise SecretManagerError("MongoDB URL not available in secrets or environment")
raise SecretManagerError("MongoDB URL not available in secrets or environment") from None
return url
@ -229,7 +229,7 @@ async def get_redis_url() -> str:
# Fallback to environment variable
url = os.getenv("REDIS_URL")
if not url:
raise SecretManagerError("Redis URL not available in secrets or environment")
raise SecretManagerError("Redis URL not available in secrets or environment") from None
return url

View file

@ -232,7 +232,7 @@ class TTSService:
audio_segments = []
current_audio_position = 0.0 # Track actual audio timeline position
for i, cue in enumerate(cues):
for _i, cue in enumerate(cues):
# Calculate where this cue should start (anchored to VTT timing)
target_start_time = cue["start_time"]
@ -298,7 +298,7 @@ class TTSService:
audio_segments = []
current_audio_position = 0.0 # Track actual audio timeline position
for i, cue in enumerate(cues):
for _i, cue in enumerate(cues):
# Calculate where this cue should start (anchored to VTT timing)
target_start_time = cue["start_time"]

View file

@ -231,7 +231,7 @@ class VideoRendererService:
error_detail = e.response.json().get("detail", str(e))
except Exception:
error_detail = str(e)
raise FFmpegExecutionError(f"Cloud Run {endpoint} failed: {error_detail}")
raise FFmpegExecutionError(f"Cloud Run {endpoint} failed: {error_detail}") from e
async def _dispatch_ffmpeg(self, cmd: list[str], timeout: int = 3600) -> dict[str, Any]:
"""
@ -391,8 +391,7 @@ class VideoRendererService:
logger.info(f"Starting overlay render for {source_video_path}")
placements = analysis.get("placements", [])
with tempfile.TemporaryDirectory() as temp_dir:
temp_dir_path = Path(temp_dir)
with tempfile.TemporaryDirectory() as _temp_dir:
# Get source video duration
duration = await self._get_video_duration(source_video_path)
@ -415,7 +414,7 @@ class VideoRendererService:
filter_parts = []
# Add each AD segment as input
for cue_index, mp3_path in ad_segments:
for _cue_index, mp3_path in ad_segments:
inputs.extend(["-i", mp3_path])
# Build complex filter
@ -429,7 +428,7 @@ class VideoRendererService:
# Add delay to each AD segment and mix
ad_labels = []
for i, (cue_index, mp3_path) in enumerate(ad_segments):
for i, (cue_index, _mp3_path) in enumerate(ad_segments):
# Find the placement for this cue
placement = next(
(p for p in placements if p.get("ad_cue_index") == cue_index),
@ -564,7 +563,7 @@ class VideoRendererService:
logger.info(f"Source Properties: {video_props}, Duration: {source_duration:.2f}s")
# Create a mapping of cue_index to mp3_path
cue_to_mp3 = {cue_index: mp3_path for cue_index, mp3_path in ad_segments}
cue_to_mp3 = dict(ad_segments)
# Pre-process placements and validate
valid_placements = []
@ -884,9 +883,6 @@ class VideoRendererService:
# Pause point is at the START of the freeze frame in the rendered timeline
pause_ms = freeze_frame_starts.get(cue_index, p["pause_point"] * 1000)
# Find the freeze segment for this cue to get its end position
freeze_seg = next((s for s in segment_metadata_list if s.is_freeze_frame and s.cue_index == cue_index), None)
# Compute min bound: end of previous AD segment (or 0 for first)
if idx == 0:
min_bound_ms = 0.0

View file

@ -190,7 +190,7 @@ async def transcribe(request: TranscribeRequest):
raise
except Exception as e:
logger.error(f"Transcription failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None
finally:
# Clean up temp file
if os.path.exists(tmp_path):
@ -252,7 +252,7 @@ async def transcribe_with_gaps(request: TranscribeWithGapsRequest):
raise
except Exception as e:
logger.error(f"Transcription with gaps failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None
finally:
# Clean up temp file
if os.path.exists(tmp_path):
@ -297,7 +297,7 @@ async def refine_pause_points(request: RefinePausePointsRequest):
except Exception as e:
logger.error(f"Pause point refinement failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e)) from None
# Startup event to pre-load Whisper model

View file

@ -1,5 +1,15 @@
import threading
import time
from celery import Celery
from celery.signals import task_failure, task_retry, task_success
from celery.signals import (
task_failure,
task_prerun,
task_received,
task_retry,
task_success,
worker_ready,
)
from ..core.config import settings
from ..core.logging import get_logger
@ -49,13 +59,6 @@ def test_task(message="test"):
return f"Test task completed: {message}"
# Add task received handler for debugging
import threading
import time
from celery.signals import task_prerun, task_received, worker_ready
@worker_ready.connect
def worker_ready_handler(sender=None, **kwargs):
"""Log when worker is ready and start heartbeat"""

View file

@ -272,7 +272,7 @@ async def _async_rerender_accessible_video(
# Validate VTT cue count matches MP3 count
vtt_cues = VTTParser.parse(ad_vtt_content)
downloaded_indices = set(idx for idx, _ in ad_segments)
downloaded_indices = {idx for idx, _ in ad_segments}
if len(vtt_cues) != len(ad_segments):
missing_indices = set(range(len(vtt_cues))) - downloaded_indices
logger.warning(

View file

@ -255,7 +255,9 @@ async def _async_translate_and_synthesize(job_id: str, languages: list[str] | No
lang_out["sdh_captions_vtt_gcs"] = sdh_gcs_uri
try:
from ..services.descriptive_transcript import generate_descriptive_transcript
from ..services.descriptive_transcript import (
generate_descriptive_transcript,
)
transcript_text = generate_descriptive_transcript(translated_captions, translated_ad)
if transcript_text:
transcript_gcs_uri = await upload_vtt_to_gcs(
@ -583,7 +585,6 @@ async def _generate_language_tts(job_id: str, language: str, lang_output: dict,
)
# Preflight budget check before dispatching TTS
tts_provider = tts_preferences.get("provider", "gemini")
from .tts_synthesis import _TTS_MODEL_STRINGS
tts_model_key = tts_preferences.get("model", "flash")
await cost_tracker.aio_preflight(
@ -646,7 +647,7 @@ async def _generate_language_tts(job_id: str, language: str, lang_output: dict,
cue_index=0,
cue_text="",
api_response_info=str(e)
)
) from e
# Handle case where results contain exceptions (shouldn't happen with new task design, but safety net)
processed_results = []

View file

@ -193,7 +193,7 @@ def synthesize_cue_task(
f"Retrying TTS cue {cue_index} in {delay:.1f}s "
f"(attempt {self.request.retries + 2}/{self.max_retries + 1})"
)
raise self.retry(exc=e, countdown=delay)
raise self.retry(exc=e, countdown=delay) from e
else:
# Max retries exhausted - return failure result instead of raising
logger.error(