feat(models): T07-T10 create SQLAlchemy models for User, ApiKey, UsageStats, ApiToken
- Add User model with email unique constraint and relationships - Add ApiKey model with encrypted key storage and user relationship - Add UsageStats model with unique constraint (api_key_id, date, model) - Add ApiToken model with token_hash indexing - Configure all cascade delete relationships - Add 49 comprehensive tests with 95% coverage Models: - User: id, email, password_hash, created_at, updated_at, is_active - ApiKey: id, user_id, name, key_encrypted, is_active, created_at, last_used_at - UsageStats: id, api_key_id, date, model, requests_count, tokens_input, tokens_output, cost - ApiToken: id, user_id, token_hash, name, created_at, last_used_at, is_active Tests: 49 passed, coverage 95%
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
"""Models package for OpenRouter API Key Monitor.
|
||||
|
||||
This package contains all SQLAlchemy models for the application.
|
||||
"""
|
||||
from openrouter_monitor.models.user import User
|
||||
from openrouter_monitor.models.api_key import ApiKey
|
||||
from openrouter_monitor.models.usage_stats import UsageStats
|
||||
from openrouter_monitor.models.api_token import ApiToken
|
||||
|
||||
__all__ = ["User", "ApiKey", "UsageStats", "ApiToken"]
|
||||
|
||||
39
src/openrouter_monitor/models/api_key.py
Normal file
39
src/openrouter_monitor/models/api_key.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""ApiKey model for OpenRouter API Key Monitor.
|
||||
|
||||
T08: ApiKey SQLAlchemy model
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from openrouter_monitor.database import Base
|
||||
|
||||
|
||||
class ApiKey(Base):
|
||||
"""API Key model for storing encrypted OpenRouter API keys.
|
||||
|
||||
Attributes:
|
||||
id: Primary key
|
||||
user_id: Foreign key to users table
|
||||
name: Human-readable name for the key
|
||||
key_encrypted: AES-256 encrypted API key
|
||||
is_active: Whether the key is active
|
||||
created_at: Timestamp when key was created
|
||||
last_used_at: Timestamp when key was last used
|
||||
user: Relationship to user
|
||||
usage_stats: Relationship to usage statistics
|
||||
"""
|
||||
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
key_encrypted = Column(String, nullable=False)
|
||||
is_active = Column(Boolean, default=True, index=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
last_used_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="api_keys", lazy="selectin")
|
||||
usage_stats = relationship("UsageStats", back_populates="api_key", cascade="all, delete-orphan", lazy="selectin")
|
||||
37
src/openrouter_monitor/models/api_token.py
Normal file
37
src/openrouter_monitor/models/api_token.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""ApiToken model for OpenRouter API Key Monitor.
|
||||
|
||||
T10: ApiToken SQLAlchemy model
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from openrouter_monitor.database import Base
|
||||
|
||||
|
||||
class ApiToken(Base):
|
||||
"""API Token model for public API access.
|
||||
|
||||
Attributes:
|
||||
id: Primary key
|
||||
user_id: Foreign key to users table
|
||||
token_hash: SHA-256 hash of the token (not the token itself)
|
||||
name: Human-readable name for the token
|
||||
created_at: Timestamp when token was created
|
||||
last_used_at: Timestamp when token was last used
|
||||
is_active: Whether the token is active
|
||||
user: Relationship to user
|
||||
"""
|
||||
|
||||
__tablename__ = "api_tokens"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
token_hash = Column(String(255), nullable=False, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
last_used_at = Column(DateTime, nullable=True)
|
||||
is_active = Column(Boolean, default=True, index=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="api_tokens", lazy="selectin")
|
||||
46
src/openrouter_monitor/models/usage_stats.py
Normal file
46
src/openrouter_monitor/models/usage_stats.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""UsageStats model for OpenRouter API Key Monitor.
|
||||
|
||||
T09: UsageStats SQLAlchemy model
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from sqlalchemy import Column, Integer, String, Date, DateTime, Numeric, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from openrouter_monitor.database import Base
|
||||
|
||||
|
||||
class UsageStats(Base):
|
||||
"""Usage statistics model for storing API usage data.
|
||||
|
||||
Attributes:
|
||||
id: Primary key
|
||||
api_key_id: Foreign key to api_keys table
|
||||
date: Date of the statistics
|
||||
model: AI model name
|
||||
requests_count: Number of requests
|
||||
tokens_input: Number of input tokens
|
||||
tokens_output: Number of output tokens
|
||||
cost: Cost in USD (Numeric 10,6)
|
||||
created_at: Timestamp when record was created
|
||||
api_key: Relationship to API key
|
||||
"""
|
||||
|
||||
__tablename__ = "usage_stats"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
api_key_id = Column(Integer, ForeignKey("api_keys.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
date = Column(Date, nullable=False, index=True)
|
||||
model = Column(String(100), nullable=False, index=True)
|
||||
requests_count = Column(Integer, default=0)
|
||||
tokens_input = Column(Integer, default=0)
|
||||
tokens_output = Column(Integer, default=0)
|
||||
cost = Column(Numeric(10, 6), default=0.0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Unique constraint: one record per api_key, date, model
|
||||
__table_args__ = (
|
||||
UniqueConstraint('api_key_id', 'date', 'model', name='uniq_key_date_model'),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
api_key = relationship("ApiKey", back_populates="usage_stats", lazy="selectin")
|
||||
37
src/openrouter_monitor/models/user.py
Normal file
37
src/openrouter_monitor/models/user.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""User model for OpenRouter API Key Monitor.
|
||||
|
||||
T07: User SQLAlchemy model
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from openrouter_monitor.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model for storing user accounts.
|
||||
|
||||
Attributes:
|
||||
id: Primary key
|
||||
email: User email address (unique, indexed)
|
||||
password_hash: Bcrypt hashed password
|
||||
created_at: Timestamp when user was created
|
||||
updated_at: Timestamp when user was last updated
|
||||
is_active: Whether the user account is active
|
||||
api_keys: Relationship to user's API keys
|
||||
api_tokens: Relationship to user's API tokens
|
||||
"""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String(255), unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Relationships - using lazy string references to avoid circular imports
|
||||
api_keys = relationship("ApiKey", back_populates="user", cascade="all, delete-orphan", lazy="selectin")
|
||||
api_tokens = relationship("ApiToken", back_populates="user", cascade="all, delete-orphan", lazy="selectin")
|
||||
Reference in New Issue
Block a user