[INIT-001] Initial project setup with Clean Architecture (feat)
Some checks failed
CD - Build & Deploy / build-and-push (push) Has been cancelled
CD - Build & Deploy / package-helm (push) Has been cancelled
CD - Build & Deploy / deploy-staging (push) Has been cancelled
CD - Build & Deploy / deploy-production (push) Has been cancelled
CD - Build & Deploy / release (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / security (push) Has been cancelled

- Implemented Clean Architecture with Domain, Application, Infrastructure, Presentation layers
- Added comprehensive project structure following SOLID principles
- Created Kubernetes deployment with Helm charts (HPA, PDB, NetworkPolicy)
- Configured ArgoCD for automated deployment (production + staging)
- Implemented CI/CD pipeline with GitHub Actions
- Added comprehensive documentation (handbook, architecture, coding standards)
- Configured PostgreSQL, Redis, Celery for backend services
- Created modern landing page with Persian fonts (Vazirmatn)
- Added Docker multi-stage build for production
- Configured development tools (pytest, black, flake8, mypy, isort)
- Added pre-commit hooks for code quality
- Implemented Makefile for common operations
This commit is contained in:
Ehsan.Asadi
2025-12-26 15:52:50 +03:30
commit 8a924f6091
135 changed files with 8637 additions and 0 deletions

0
src/__init__.py Normal file
View File

0
src/config/__init__.py Normal file
View File

106
src/config/cache.py Normal file
View File

@@ -0,0 +1,106 @@
"""Redis cache configuration and management.
This module handles Redis connections and caching operations.
"""
from typing import Optional, Any
from redis import Redis, ConnectionPool
import json
from src.config.settings import settings
# Redis Connection Pool
redis_pool = ConnectionPool.from_url(
settings.REDIS_URL,
max_connections=settings.REDIS_MAX_CONNECTIONS,
decode_responses=True,
)
# Redis Client
redis_client = Redis(connection_pool=redis_pool)
def get_redis() -> Redis:
"""Get Redis client instance.
Returns:
Redis: Redis client
"""
return redis_client
def cache_set(
key: str,
value: Any,
ttl: Optional[int] = None
) -> bool:
"""Set cache value.
Args:
key: Cache key
value: Value to cache
ttl: Time to live in seconds
Returns:
bool: True if successful
"""
try:
serialized_value = json.dumps(value)
if ttl:
return redis_client.setex(key, ttl, serialized_value)
return redis_client.set(key, serialized_value)
except Exception:
return False
def cache_get(key: str) -> Optional[Any]:
"""Get cache value.
Args:
key: Cache key
Returns:
Optional[Any]: Cached value or None
"""
try:
value = redis_client.get(key)
if value:
return json.loads(value)
return None
except Exception:
return None
def cache_delete(key: str) -> bool:
"""Delete cache value.
Args:
key: Cache key
Returns:
bool: True if successful
"""
try:
return bool(redis_client.delete(key))
except Exception:
return False
def cache_clear_pattern(pattern: str) -> int:
"""Clear cache by pattern.
Args:
pattern: Key pattern (e.g., "user:*")
Returns:
int: Number of keys deleted
"""
try:
keys = redis_client.keys(pattern)
if keys:
return redis_client.delete(*keys)
return 0
except Exception:
return 0

85
src/config/database.py Normal file
View File

@@ -0,0 +1,85 @@
"""Database configuration and connection management.
This module handles database connections, session management, and connection pooling.
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.ext.declarative import declarative_base
from contextlib import contextmanager
from typing import Generator
from src.config.settings import settings
# SQLAlchemy Base
Base = declarative_base()
# Database Engine
engine = create_engine(
settings.DATABASE_URL,
pool_size=settings.DATABASE_POOL_SIZE,
max_overflow=settings.DATABASE_MAX_OVERFLOW,
pool_pre_ping=True, # Enable connection health checks
echo=settings.DEBUG, # Log SQL queries in debug mode
)
# Session Factory
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine
)
def get_session() -> Generator[Session, None, None]:
"""Get database session.
Yields:
Session: SQLAlchemy session
Example:
>>> with get_session() as session:
... user = session.query(User).first()
"""
session = SessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
@contextmanager
def get_db_context() -> Generator[Session, None, None]:
"""Context manager for database session.
Yields:
Session: SQLAlchemy session
"""
session = SessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
def init_db() -> None:
"""Initialize database (create all tables).
Note: In production, use Alembic migrations instead.
"""
Base.metadata.create_all(bind=engine)
def close_db() -> None:
"""Close database connections."""
engine.dispose()

View File

56
src/config/logging.py Normal file
View File

@@ -0,0 +1,56 @@
"""Logging configuration using structlog.
This module sets up structured logging for the application.
"""
import logging
import structlog
from typing import Any
from src.config.settings import settings
def setup_logging() -> None:
"""Configure structured logging with structlog."""
# Configure structlog
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
structlog.dev.set_exc_info,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.JSONRenderer() if settings.LOG_FORMAT == "json"
else structlog.dev.ConsoleRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(
logging.getLevelName(settings.LOG_LEVEL)
),
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)
# Configure standard logging
logging.basicConfig(
format="%(message)s",
level=logging.getLevelName(settings.LOG_LEVEL),
)
def get_logger(name: str) -> Any:
"""Get a logger instance.
Args:
name: Logger name (usually __name__)
Returns:
Logger instance
Example:
>>> logger = get_logger(__name__)
>>> logger.info("user_created", user_id=123, email="test@example.com")
"""
return structlog.get_logger(name)

74
src/config/settings.py Normal file
View File

@@ -0,0 +1,74 @@
"""Main configuration settings for Peikarband platform.
This module contains all configuration settings loaded from environment variables.
Follows PEP 8 and clean code principles.
"""
from typing import Optional
from pydantic import BaseSettings, Field, validator
import os
class Settings(BaseSettings):
"""Main settings class using Pydantic for validation."""
# Application
APP_NAME: str = "Peikarband"
APP_VERSION: str = "0.1.0"
DEBUG: bool = Field(default=False, env="DEBUG")
ENVIRONMENT: str = Field(default="development", env="ENVIRONMENT")
# Database
DATABASE_URL: str = Field(..., env="DATABASE_URL")
DATABASE_POOL_SIZE: int = Field(default=20, env="DATABASE_POOL_SIZE")
DATABASE_MAX_OVERFLOW: int = Field(default=10, env="DATABASE_MAX_OVERFLOW")
# Redis
REDIS_URL: str = Field(..., env="REDIS_URL")
REDIS_MAX_CONNECTIONS: int = Field(default=50, env="REDIS_MAX_CONNECTIONS")
# Security
SECRET_KEY: str = Field(..., env="SECRET_KEY")
JWT_SECRET_KEY: str = Field(..., env="JWT_SECRET_KEY")
JWT_ALGORITHM: str = Field(default="HS256", env="JWT_ALGORITHM")
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, env="JWT_ACCESS_TOKEN_EXPIRE_MINUTES")
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, env="JWT_REFRESH_TOKEN_EXPIRE_DAYS")
# Celery
CELERY_BROKER_URL: str = Field(..., env="CELERY_BROKER_URL")
CELERY_RESULT_BACKEND: str = Field(..., env="CELERY_RESULT_BACKEND")
# External APIs
DIGITALOCEAN_API_KEY: Optional[str] = Field(default=None, env="DIGITALOCEAN_API_KEY")
HETZNER_API_KEY: Optional[str] = Field(default=None, env="HETZNER_API_KEY")
OVH_API_KEY: Optional[str] = Field(default=None, env="OVH_API_KEY")
# Payment Gateways
ZARINPAL_MERCHANT_ID: Optional[str] = Field(default=None, env="ZARINPAL_MERCHANT_ID")
IDPAY_API_KEY: Optional[str] = Field(default=None, env="IDPAY_API_KEY")
# Logging
LOG_LEVEL: str = Field(default="INFO", env="LOG_LEVEL")
LOG_FORMAT: str = Field(default="json", env="LOG_FORMAT")
# CORS
CORS_ORIGINS: list[str] = Field(default=["*"], env="CORS_ORIGINS")
class Config:
"""Pydantic configuration."""
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
@validator("ENVIRONMENT")
def validate_environment(cls, v: str) -> str:
"""Validate environment value."""
allowed = ["development", "staging", "production"]
if v not in allowed:
raise ValueError(f"ENVIRONMENT must be one of {allowed}")
return v
# Global settings instance
settings = Settings()

0
src/core/__init__.py Normal file
View File

View File

View File

View File

View File

View File

@@ -0,0 +1,67 @@
"""Base entity for domain layer.
This module defines the base entity class that all domain entities inherit from.
"""
from datetime import datetime
from typing import Optional
class BaseEntity:
"""Base domain entity.
All domain entities should inherit from this class.
This is NOT a database model, but a pure domain object.
Attributes:
id: Entity identifier
created_at: Creation timestamp
updated_at: Last update timestamp
"""
def __init__(
self,
id: Optional[int] = None,
created_at: Optional[datetime] = None,
updated_at: Optional[datetime] = None
):
"""Initialize base entity.
Args:
id: Entity identifier
created_at: Creation timestamp
updated_at: Last update timestamp
"""
self.id = id
self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow()
def __eq__(self, other: object) -> bool:
"""Check equality based on ID.
Args:
other: Other object
Returns:
bool: True if same entity
"""
if not isinstance(other, self.__class__):
return False
return self.id == other.id
def __hash__(self) -> int:
"""Hash based on ID.
Returns:
int: Hash value
"""
return hash(self.id)
def __repr__(self) -> str:
"""String representation.
Returns:
str: String representation
"""
return f"<{self.__class__.__name__}(id={self.id})>"

View File

View File

View File

@@ -0,0 +1,59 @@
"""Base exceptions for domain layer.
This module defines the exception hierarchy for the domain layer.
"""
class PeikarbandException(Exception):
"""Base exception for all custom exceptions."""
def __init__(self, message: str, code: str = "INTERNAL_ERROR"):
"""Initialize exception.
Args:
message: Error message
code: Error code
"""
self.message = message
self.code = code
super().__init__(self.message)
class DomainException(PeikarbandException):
"""Base exception for domain layer."""
def __init__(self, message: str, code: str = "DOMAIN_ERROR"):
"""Initialize domain exception.
Args:
message: Error message
code: Error code
"""
super().__init__(message, code)
class ValidationException(DomainException):
"""Validation error in domain."""
def __init__(self, message: str, field: str = ""):
"""Initialize validation exception.
Args:
message: Error message
field: Field that failed validation
"""
self.field = field
super().__init__(message, "VALIDATION_ERROR")
class BusinessRuleException(DomainException):
"""Business rule violation."""
def __init__(self, message: str):
"""Initialize business rule exception.
Args:
message: Error message
"""
super().__init__(message, "BUSINESS_RULE_VIOLATION")

View File

@@ -0,0 +1,104 @@
"""User-related domain exceptions."""
from src.core.domain.exceptions.base import DomainException, ValidationException
class UserNotFoundException(DomainException):
"""User not found exception."""
def __init__(self, user_id: int = None, email: str = None):
"""Initialize exception.
Args:
user_id: User ID
email: User email
"""
if user_id:
message = f"User with ID {user_id} not found"
elif email:
message = f"User with email {email} not found"
else:
message = "User not found"
super().__init__(message, "USER_NOT_FOUND")
class EmailAlreadyExistsException(DomainException):
"""Email already exists exception."""
def __init__(self, email: str):
"""Initialize exception.
Args:
email: Email address
"""
super().__init__(
f"Email {email} is already registered",
"EMAIL_ALREADY_EXISTS"
)
class InvalidEmailException(ValidationException):
"""Invalid email format exception."""
def __init__(self, email: str):
"""Initialize exception.
Args:
email: Invalid email
"""
super().__init__(
f"Invalid email format: {email}",
field="email"
)
class WeakPasswordException(ValidationException):
"""Weak password exception."""
def __init__(self, reason: str = "Password does not meet requirements"):
"""Initialize exception.
Args:
reason: Reason why password is weak
"""
super().__init__(reason, field="password")
class InvalidCredentialsException(DomainException):
"""Invalid login credentials exception."""
def __init__(self):
"""Initialize exception."""
super().__init__(
"Invalid email or password",
"INVALID_CREDENTIALS"
)
class AccountLockedException(DomainException):
"""Account is locked exception."""
def __init__(self, reason: str = "Account is locked"):
"""Initialize exception.
Args:
reason: Reason for lock
"""
super().__init__(reason, "ACCOUNT_LOCKED")
class InsufficientBalanceException(DomainException):
"""Insufficient wallet balance exception."""
def __init__(self, required: str, available: str):
"""Initialize exception.
Args:
required: Required amount
available: Available amount
"""
super().__init__(
f"Insufficient balance. Required: {required}, Available: {available}",
"INSUFFICIENT_BALANCE"
)

View File

@@ -0,0 +1,94 @@
"""Email value object.
Email is an immutable value object that represents a validated email address.
"""
import re
from typing import Any
class Email:
"""Email value object.
Represents a validated email address.
Immutable - once created, cannot be changed.
Attributes:
value: The email address string
"""
EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
def __init__(self, value: str):
"""Initialize email.
Args:
value: Email address string
Raises:
ValueError: If email format is invalid
"""
if not self._is_valid(value):
raise ValueError(f"Invalid email format: {value}")
self._value = value.lower().strip()
@property
def value(self) -> str:
"""Get email value.
Returns:
str: Email address
"""
return self._value
@staticmethod
def _is_valid(email: str) -> bool:
"""Validate email format.
Args:
email: Email string to validate
Returns:
bool: True if valid
"""
if not email or not isinstance(email, str):
return False
return bool(Email.EMAIL_REGEX.match(email.strip()))
def __str__(self) -> str:
"""String representation.
Returns:
str: Email address
"""
return self._value
def __repr__(self) -> str:
"""Developer representation.
Returns:
str: Email representation
"""
return f"Email('{self._value}')"
def __eq__(self, other: Any) -> bool:
"""Check equality.
Args:
other: Other object
Returns:
bool: True if equal
"""
if not isinstance(other, Email):
return False
return self._value == other._value
def __hash__(self) -> int:
"""Hash value.
Returns:
int: Hash
"""
return hash(self._value)

View File

@@ -0,0 +1,202 @@
"""Money value object.
Money represents a monetary amount with currency.
"""
from decimal import Decimal
from typing import Any, Union
class Money:
"""Money value object.
Represents a monetary amount with currency.
Immutable and provides monetary operations.
Attributes:
amount: Decimal amount
currency: Currency code (e.g., 'IRR', 'USD')
"""
def __init__(self, amount: Union[Decimal, int, float, str], currency: str = "IRR"):
"""Initialize money.
Args:
amount: Monetary amount
currency: Currency code (default: IRR for Iranian Rial)
Raises:
ValueError: If amount is negative or currency is invalid
"""
self._amount = Decimal(str(amount))
if self._amount < 0:
raise ValueError("Amount cannot be negative")
if not currency or len(currency) != 3:
raise ValueError(f"Invalid currency code: {currency}")
self._currency = currency.upper()
@property
def amount(self) -> Decimal:
"""Get amount.
Returns:
Decimal: Amount
"""
return self._amount
@property
def currency(self) -> str:
"""Get currency.
Returns:
str: Currency code
"""
return self._currency
def add(self, other: "Money") -> "Money":
"""Add two money values.
Args:
other: Other money value
Returns:
Money: New money object with sum
Raises:
ValueError: If currencies don't match
"""
self._check_currency(other)
return Money(self._amount + other._amount, self._currency)
def subtract(self, other: "Money") -> "Money":
"""Subtract money value.
Args:
other: Other money value
Returns:
Money: New money object with difference
Raises:
ValueError: If currencies don't match or result is negative
"""
self._check_currency(other)
result = self._amount - other._amount
if result < 0:
raise ValueError("Result cannot be negative")
return Money(result, self._currency)
def multiply(self, multiplier: Union[int, float, Decimal]) -> "Money":
"""Multiply amount by a factor.
Args:
multiplier: Multiplication factor
Returns:
Money: New money object
"""
result = self._amount * Decimal(str(multiplier))
return Money(result, self._currency)
def _check_currency(self, other: "Money") -> None:
"""Check if currencies match.
Args:
other: Other money value
Raises:
ValueError: If currencies don't match
"""
if self._currency != other._currency:
raise ValueError(
f"Currency mismatch: {self._currency} vs {other._currency}"
)
def __str__(self) -> str:
"""String representation.
Returns:
str: Formatted money string
"""
return f"{self._amount:,.2f} {self._currency}"
def __repr__(self) -> str:
"""Developer representation.
Returns:
str: Money representation
"""
return f"Money({self._amount}, '{self._currency}')"
def __eq__(self, other: Any) -> bool:
"""Check equality.
Args:
other: Other object
Returns:
bool: True if equal
"""
if not isinstance(other, Money):
return False
return self._amount == other._amount and self._currency == other._currency
def __lt__(self, other: "Money") -> bool:
"""Less than comparison.
Args:
other: Other money value
Returns:
bool: True if less than
"""
self._check_currency(other)
return self._amount < other._amount
def __le__(self, other: "Money") -> bool:
"""Less than or equal comparison.
Args:
other: Other money value
Returns:
bool: True if less than or equal
"""
self._check_currency(other)
return self._amount <= other._amount
def __gt__(self, other: "Money") -> bool:
"""Greater than comparison.
Args:
other: Other money value
Returns:
bool: True if greater than
"""
self._check_currency(other)
return self._amount > other._amount
def __ge__(self, other: "Money") -> bool:
"""Greater than or equal comparison.
Args:
other: Other money value
Returns:
bool: True if greater than or equal
"""
self._check_currency(other)
return self._amount >= other._amount
def __hash__(self) -> int:
"""Hash value.
Returns:
int: Hash
"""
return hash((self._amount, self._currency))

View File

@@ -0,0 +1,140 @@
"""Phone number value object.
Phone represents a validated phone number.
"""
import re
from typing import Any
class Phone:
"""Phone number value object.
Represents a validated Iranian phone number.
Supports both mobile and landline numbers.
Attributes:
value: Phone number string (normalized format)
"""
# Iranian mobile: starts with 09, 10 digits
MOBILE_REGEX = re.compile(r'^09\d{9}$')
# International format: +989...
INTERNATIONAL_REGEX = re.compile(r'^\+989\d{9}$')
def __init__(self, value: str):
"""Initialize phone number.
Args:
value: Phone number string
Raises:
ValueError: If phone format is invalid
"""
normalized = self._normalize(value)
if not self._is_valid(normalized):
raise ValueError(f"Invalid phone number: {value}")
self._value = normalized
@property
def value(self) -> str:
"""Get phone number.
Returns:
str: Phone number
"""
return self._value
@property
def international_format(self) -> str:
"""Get international format.
Returns:
str: +989XXXXXXXXX format
"""
if self._value.startswith('+'):
return self._value
return f"+98{self._value[1:]}"
@staticmethod
def _normalize(phone: str) -> str:
"""Normalize phone number.
Removes spaces, dashes, parentheses.
Args:
phone: Phone string
Returns:
str: Normalized phone
"""
if not phone:
return ""
# Remove common separators
phone = re.sub(r'[\s\-\(\)]', '', phone.strip())
# Convert international format to local
if phone.startswith('+98'):
phone = '0' + phone[3:]
elif phone.startswith('0098'):
phone = '0' + phone[4:]
elif phone.startswith('98') and len(phone) == 12:
phone = '0' + phone[2:]
return phone
@staticmethod
def _is_valid(phone: str) -> bool:
"""Validate phone number.
Args:
phone: Normalized phone string
Returns:
bool: True if valid
"""
if not phone or not isinstance(phone, str):
return False
# Check if matches mobile pattern
return bool(Phone.MOBILE_REGEX.match(phone))
def __str__(self) -> str:
"""String representation.
Returns:
str: Phone number
"""
return self._value
def __repr__(self) -> str:
"""Developer representation.
Returns:
str: Phone representation
"""
return f"Phone('{self._value}')"
def __eq__(self, other: Any) -> bool:
"""Check equality.
Args:
other: Other object
Returns:
bool: True if equal
"""
if not isinstance(other, Phone):
return False
return self._value == other._value
def __hash__(self) -> int:
"""Hash value.
Returns:
int: Hash
"""
return hash(self._value)

View File

View File

0
src/infrastructure/cache/__init__.py vendored Normal file
View File

View File

View File

@@ -0,0 +1,87 @@
"""Alembic environment configuration.
This module is used by Alembic to manage database migrations.
"""
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parents[4]
sys.path.insert(0, str(project_root))
from src.config.settings import settings
from src.config.database import Base
# Import all models to ensure they're registered with Base.metadata
from src.infrastructure.database.models import * # noqa
# Alembic Config object
config = context.config
# Set database URL from settings
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
# Interpret the config file for Python logging
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Target metadata for autogenerate
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,27 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade database schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade database schema."""
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,51 @@
"""Base model for all database models.
This module defines the base class that all SQLAlchemy models inherit from.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declared_attr
from src.config.database import Base as DeclarativeBase
class BaseModel(DeclarativeBase):
"""Base model with common fields.
All models should inherit from this class to get:
- id (primary key)
- created_at
- updated_at
- Automatic table naming
"""
__abstract__ = True
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime,
default=datetime.utcnow,
onupdate=datetime.utcnow,
nullable=False
)
@declared_attr
def __tablename__(cls) -> str:
"""Generate table name from class name.
Converts PascalCase to snake_case.
Example: UserModel -> user_model
"""
import re
name = cls.__name__
# Convert PascalCase to snake_case
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
return name
def __repr__(self) -> str:
"""String representation."""
return f"<{self.__class__.__name__}(id={self.id})>"

View File

@@ -0,0 +1,122 @@
"""Base repository implementation.
This module provides a base repository class with common CRUD operations.
"""
from typing import TypeVar, Generic, Optional, List, Type
from sqlalchemy.orm import Session
from src.infrastructure.database.models.base import BaseModel
ModelType = TypeVar("ModelType", bound=BaseModel)
class BaseRepository(Generic[ModelType]):
"""Base repository with common database operations.
Provides basic CRUD operations that can be inherited by specific repositories.
Attributes:
model: SQLAlchemy model class
session: Database session
"""
def __init__(self, model: Type[ModelType], session: Session):
"""Initialize repository.
Args:
model: SQLAlchemy model class
session: Database session
"""
self.model = model
self._session = session
def get_by_id(self, id: int) -> Optional[ModelType]:
"""Get entity by ID.
Args:
id: Entity ID
Returns:
Optional[ModelType]: Entity or None
"""
return self._session.query(self.model).filter(self.model.id == id).first()
def get_all(self, skip: int = 0, limit: int = 100) -> List[ModelType]:
"""Get all entities with pagination.
Args:
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
List[ModelType]: List of entities
"""
return self._session.query(self.model).offset(skip).limit(limit).all()
def create(self, entity: ModelType) -> ModelType:
"""Create new entity.
Args:
entity: Entity to create
Returns:
ModelType: Created entity
"""
self._session.add(entity)
self._session.flush()
self._session.refresh(entity)
return entity
def update(self, entity: ModelType) -> ModelType:
"""Update existing entity.
Args:
entity: Entity to update
Returns:
ModelType: Updated entity
"""
self._session.merge(entity)
self._session.flush()
self._session.refresh(entity)
return entity
def delete(self, id: int) -> bool:
"""Delete entity by ID.
Args:
id: Entity ID
Returns:
bool: True if deleted, False if not found
"""
entity = self.get_by_id(id)
if entity:
self._session.delete(entity)
self._session.flush()
return True
return False
def count(self) -> int:
"""Count total number of entities.
Returns:
int: Total count
"""
return self._session.query(self.model).count()
def exists(self, id: int) -> bool:
"""Check if entity exists.
Args:
id: Entity ID
Returns:
bool: True if exists
"""
return self._session.query(
self._session.query(self.model).filter(self.model.id == id).exists()
).scalar()

View File

@@ -0,0 +1,81 @@
"""Unit of Work pattern implementation.
This module implements the Unit of Work pattern for managing database transactions.
"""
from typing import Optional
from sqlalchemy.orm import Session
from src.config.database import SessionLocal
class UnitOfWork:
"""Unit of Work for managing database transactions.
Usage:
with UnitOfWork() as uow:
user = uow.users.get_by_id(1)
user.email = "new@email.com"
uow.users.update(user)
# Automatically commits on success
"""
def __init__(self, session_factory=SessionLocal):
"""Initialize Unit of Work.
Args:
session_factory: Factory to create database sessions
"""
self.session_factory = session_factory
self._session: Optional[Session] = None
def __enter__(self) -> "UnitOfWork":
"""Enter context manager.
Returns:
UnitOfWork: Self
"""
self._session = self.session_factory()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context manager.
Commits on success, rollbacks on error.
"""
if exc_type is not None:
self.rollback()
else:
self.commit()
self.close()
def commit(self) -> None:
"""Commit the current transaction."""
if self._session:
self._session.commit()
def rollback(self) -> None:
"""Rollback the current transaction."""
if self._session:
self._session.rollback()
def close(self) -> None:
"""Close the session."""
if self._session:
self._session.close()
self._session = None
@property
def session(self) -> Session:
"""Get the current session.
Returns:
Session: Current database session
Raises:
RuntimeError: If session not initialized
"""
if self._session is None:
raise RuntimeError("Session not initialized. Use within context manager.")
return self._session

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,31 @@
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap');
body {
font-family: 'Vazirmatn', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes glow {
0% { box-shadow: 0 12px 40px rgba(30, 64, 175, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2); }
50% { box-shadow: 0 18px 50px rgba(30, 64, 175, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.3); }
100% { box-shadow: 0 12px 40px rgba(30, 64, 175, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2); }
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}

View File

File diff suppressed because it is too large Load Diff

View File

View File

0
src/shared/__init__.py Normal file
View File

View File

View File