feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle

Made-with: Cursor
This commit is contained in:
Sepehr Ramezani
2026-03-07 11:42:58 +01:00
parent 3d37ce4582
commit 473b3e26c7
181 changed files with 30617 additions and 7170 deletions

View File

@@ -1,19 +1,34 @@
"""
SQLAlchemy models for the Document Translation API
"""
import os
import uuid
from datetime import datetime
from typing import Optional, List
from datetime import datetime, timezone
import warnings
from sqlalchemy import (
Column, String, Integer, Float, Boolean, DateTime, Text,
ForeignKey, Enum, Index, JSON, BigInteger
Column,
String,
Integer,
Float,
Boolean,
DateTime,
Text,
ForeignKey,
Enum,
Index,
JSON,
BigInteger,
CheckConstraint,
)
from sqlalchemy.orm import relationship, declarative_base
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
import enum
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
Base = declarative_base()
@@ -22,6 +37,11 @@ def generate_uuid():
return str(uuid.uuid4())
def generate_uuid_value():
"""Generate a new UUID value for PostgreSQL UUID column"""
return uuid.uuid4()
class PlanType(str, enum.Enum):
FREE = "free"
STARTER = "starter"
@@ -40,57 +60,78 @@ class SubscriptionStatus(str, enum.Enum):
class User(Base):
"""User model for authentication and billing"""
__tablename__ = "users"
id = Column(String(36), primary_key=True, default=generate_uuid)
email = Column(String(255), unique=True, nullable=False, index=True)
name = Column(String(255), nullable=False)
password_hash = Column(String(255), nullable=False)
# Account status
hashed_password = Column(String(255), nullable=False)
tier = Column(String(10), default="free", nullable=False)
daily_translation_count = Column(Integer, default=0, nullable=False)
email_verified = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
avatar_url = Column(String(500), nullable=True)
# Subscription info
plan = Column(Enum(PlanType), default=PlanType.FREE)
subscription_status = Column(Enum(SubscriptionStatus), default=SubscriptionStatus.ACTIVE)
# Stripe integration
subscription_status = Column(
Enum(SubscriptionStatus), default=SubscriptionStatus.ACTIVE
)
stripe_customer_id = Column(String(255), nullable=True, index=True)
stripe_subscription_id = Column(String(255), nullable=True)
# Usage tracking (reset monthly)
docs_translated_this_month = Column(Integer, default=0)
pages_translated_this_month = Column(Integer, default=0)
api_calls_this_month = Column(Integer, default=0)
extra_credits = Column(Integer, default=0) # Purchased credits
usage_reset_date = Column(DateTime, default=datetime.utcnow)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
extra_credits = Column(Integer, default=0)
usage_reset_date = Column(DateTime, default=_utcnow)
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
last_login_at = Column(DateTime, nullable=True)
# Relationships
translations = relationship("Translation", back_populates="user", lazy="dynamic")
api_keys = relationship("ApiKey", back_populates="user", lazy="dynamic")
# Indexes
translations = relationship("Translation", back_populates="user", lazy="select")
api_keys = relationship("ApiKey", back_populates="user", lazy="select")
__table_args__ = (
Index('ix_users_email_active', 'email', 'is_active'),
Index('ix_users_stripe_customer', 'stripe_customer_id'),
CheckConstraint("tier IN ('free', 'pro')", name="ck_users_tier"),
Index("ix_users_email_active", "email", "is_active"),
Index("ix_users_stripe_customer", "stripe_customer_id"),
)
@property
def password_hash(self) -> str:
warnings.warn(
"password_hash is deprecated, use hashed_password instead",
DeprecationWarning,
stacklevel=2,
)
return self.hashed_password
@password_hash.setter
def password_hash(self, value: str) -> None:
warnings.warn(
"password_hash is deprecated, use hashed_password instead",
DeprecationWarning,
stacklevel=2,
)
self.hashed_password = value
def to_dict(self) -> dict:
"""Convert user to dictionary for API response"""
return {
"id": self.id,
"email": self.email,
"name": self.name,
"avatar_url": self.avatar_url,
"tier": self.tier,
"plan": self.plan.value if self.plan else "free",
"subscription_status": self.subscription_status.value if self.subscription_status else "active",
"subscription_status": self.subscription_status.value
if self.subscription_status
else "active",
"daily_translation_count": self.daily_translation_count,
"docs_translated_this_month": self.docs_translated_this_month,
"pages_translated_this_month": self.pages_translated_this_month,
"api_calls_this_month": self.api_calls_this_month,
@@ -102,44 +143,49 @@ class User(Base):
class Translation(Base):
"""Translation history for analytics and billing"""
__tablename__ = "translations"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
# File info
original_filename = Column(String(255), nullable=False)
file_type = Column(String(10), nullable=False) # xlsx, docx, pptx
file_type = Column(String(20), nullable=False) # xlsx, docx, pptx
file_size_bytes = Column(BigInteger, default=0)
page_count = Column(Integer, default=0)
# Translation details
source_language = Column(String(10), default="auto")
target_language = Column(String(10), nullable=False)
provider = Column(String(50), nullable=False) # google, deepl, ollama, etc.
# Processing info
status = Column(String(20), default="pending") # pending, processing, completed, failed
status = Column(
String(20), default="pending"
) # pending, processing, completed, failed
error_message = Column(Text, nullable=True)
processing_time_ms = Column(Integer, nullable=True)
# Cost tracking (for paid providers)
characters_translated = Column(Integer, default=0)
estimated_cost_usd = Column(Float, default=0.0)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
created_at = Column(DateTime, default=_utcnow)
completed_at = Column(DateTime, nullable=True)
# Relationship
user = relationship("User", back_populates="translations")
# Indexes
__table_args__ = (
Index('ix_translations_user_date', 'user_id', 'created_at'),
Index('ix_translations_status', 'status'),
Index("ix_translations_user_date", "user_id", "created_at"),
Index("ix_translations_status", "status"),
)
def to_dict(self) -> dict:
return {
"id": self.id,
@@ -154,43 +200,49 @@ class Translation(Base):
"processing_time_ms": self.processing_time_ms,
"characters_translated": self.characters_translated,
"created_at": self.created_at.isoformat() if self.created_at else None,
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
"completed_at": self.completed_at.isoformat()
if self.completed_at
else None,
}
class ApiKey(Base):
"""API keys for programmatic access"""
__tablename__ = "api_keys"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
# Key info
name = Column(String(100), nullable=False) # User-friendly name
key_hash = Column(String(255), nullable=False) # SHA256 of the key
key_prefix = Column(String(10), nullable=False) # First 8 chars for identification
# Permissions
is_active = Column(Boolean, default=True)
scopes = Column(JSON, default=list) # ["translate", "read", "write"]
# Usage tracking
last_used_at = Column(DateTime, nullable=True)
usage_count = Column(Integer, default=0)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
created_at = Column(DateTime, default=_utcnow)
expires_at = Column(DateTime, nullable=True)
revoked_at = Column(DateTime, nullable=True) # Set when is_active=False
# Relationship
user = relationship("User", back_populates="api_keys")
# Indexes
__table_args__ = (
Index('ix_api_keys_prefix', 'key_prefix'),
Index('ix_api_keys_hash', 'key_hash'),
Index("ix_api_keys_prefix", "key_prefix"),
Index("ix_api_keys_hash", "key_hash"),
)
def to_dict(self) -> dict:
return {
"id": self.id,
@@ -198,7 +250,9 @@ class ApiKey(Base):
"key_prefix": self.key_prefix,
"is_active": self.is_active,
"scopes": self.scopes,
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
"last_used_at": self.last_used_at.isoformat()
if self.last_used_at
else None,
"usage_count": self.usage_count,
"created_at": self.created_at.isoformat() if self.created_at else None,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
@@ -207,53 +261,149 @@ class ApiKey(Base):
class UsageLog(Base):
"""Daily usage aggregation for billing and analytics"""
__tablename__ = "usage_logs"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
# Date (for daily aggregation)
date = Column(DateTime, nullable=False, index=True)
# Aggregated counts
documents_count = Column(Integer, default=0)
pages_count = Column(Integer, default=0)
characters_count = Column(BigInteger, default=0)
api_calls_count = Column(Integer, default=0)
# By provider breakdown (JSON)
provider_breakdown = Column(JSON, default=dict)
# Indexes
__table_args__ = (
Index('ix_usage_logs_user_date', 'user_id', 'date', unique=True),
)
__table_args__ = (Index("ix_usage_logs_user_date", "user_id", "date", unique=True),)
class PaymentHistory(Base):
"""Payment and invoice history"""
__tablename__ = "payment_history"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
# Stripe info
stripe_payment_intent_id = Column(String(255), nullable=True)
stripe_invoice_id = Column(String(255), nullable=True)
# Payment details
amount_cents = Column(Integer, nullable=False)
currency = Column(String(3), default="usd")
payment_type = Column(String(50), nullable=False) # subscription, credits, one_time
status = Column(String(20), nullable=False) # succeeded, failed, pending, refunded
# Description
description = Column(String(255), nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
created_at = Column(DateTime, default=_utcnow)
# Indexes
__table_args__ = (
Index('ix_payment_history_user', 'user_id', 'created_at'),
__table_args__ = (Index("ix_payment_history_user", "user_id", "created_at"),)
class Glossary(Base):
"""User's glossary containing source->target term pairs.
Story 3.9: Glossaires - Endpoint CRUD
"""
__tablename__ = "glossaries"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
name = Column(String(255), nullable=False)
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
# Relationship
terms = relationship(
"GlossaryTerm", back_populates="glossary", cascade="all, delete-orphan"
)
# Indexes
__table_args__ = (Index("ix_glossaries_user_id", "user_id"),)
def to_dict(self) -> dict:
return {
"id": self.id,
"user_id": self.user_id,
"name": self.name,
"terms": [term.to_dict() for term in self.terms] if self.terms else [],
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
class GlossaryTerm(Base):
"""Single term pair in a glossary.
Story 3.9: Glossaires - Endpoint CRUD
"""
__tablename__ = "glossary_terms"
id = Column(String(36), primary_key=True, default=generate_uuid)
glossary_id = Column(
String(36), ForeignKey("glossaries.id", ondelete="CASCADE"), nullable=False
)
source = Column(String(500), nullable=False)
target = Column(String(500), nullable=False)
created_at = Column(DateTime, default=_utcnow)
# Relationship
glossary = relationship("Glossary", back_populates="terms")
# Indexes
__table_args__ = (Index("ix_glossary_terms_glossary_id", "glossary_id"),)
def to_dict(self) -> dict:
return {
"id": self.id,
"source": self.source,
"target": self.target,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
class CustomPrompt(Base):
"""User's custom prompts for LLM translation context.
Story 3.11: Custom Prompts - Endpoint CRUD
"""
__tablename__ = "custom_prompts"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
name = Column(String(255), nullable=False)
content = Column(Text, nullable=False)
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
# Indexes
__table_args__ = (Index("ix_custom_prompts_user_id", "user_id"),)
def to_dict(self) -> dict:
return {
"id": self.id,
"user_id": self.user_id,
"name": self.name,
"content": self.content,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}