[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
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:
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/config/__init__.py
Normal file
0
src/config/__init__.py
Normal file
106
src/config/cache.py
Normal file
106
src/config/cache.py
Normal 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
85
src/config/database.py
Normal 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()
|
||||
|
||||
0
src/config/environments/__init__.py
Normal file
0
src/config/environments/__init__.py
Normal file
56
src/config/logging.py
Normal file
56
src/config/logging.py
Normal 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
74
src/config/settings.py
Normal 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
0
src/core/__init__.py
Normal file
0
src/core/application/__init__.py
Normal file
0
src/core/application/__init__.py
Normal file
0
src/core/application/dto/__init__.py
Normal file
0
src/core/application/dto/__init__.py
Normal file
0
src/core/application/interfaces/__init__.py
Normal file
0
src/core/application/interfaces/__init__.py
Normal file
0
src/core/application/use_cases/__init__.py
Normal file
0
src/core/application/use_cases/__init__.py
Normal file
0
src/core/application/use_cases/auth/__init__.py
Normal file
0
src/core/application/use_cases/auth/__init__.py
Normal file
0
src/core/application/use_cases/billing/__init__.py
Normal file
0
src/core/application/use_cases/billing/__init__.py
Normal file
0
src/core/application/use_cases/servers/__init__.py
Normal file
0
src/core/application/use_cases/servers/__init__.py
Normal file
0
src/core/application/use_cases/services/__init__.py
Normal file
0
src/core/application/use_cases/services/__init__.py
Normal file
0
src/core/application/validators/__init__.py
Normal file
0
src/core/application/validators/__init__.py
Normal file
0
src/core/domain/__init__.py
Normal file
0
src/core/domain/__init__.py
Normal file
0
src/core/domain/entities/__init__.py
Normal file
0
src/core/domain/entities/__init__.py
Normal file
67
src/core/domain/entities/base.py
Normal file
67
src/core/domain/entities/base.py
Normal 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})>"
|
||||
|
||||
0
src/core/domain/enums/__init__.py
Normal file
0
src/core/domain/enums/__init__.py
Normal file
0
src/core/domain/exceptions/__init__.py
Normal file
0
src/core/domain/exceptions/__init__.py
Normal file
59
src/core/domain/exceptions/base.py
Normal file
59
src/core/domain/exceptions/base.py
Normal 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")
|
||||
|
||||
104
src/core/domain/exceptions/user_exceptions.py
Normal file
104
src/core/domain/exceptions/user_exceptions.py
Normal 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"
|
||||
)
|
||||
|
||||
0
src/core/domain/value_objects/__init__.py
Normal file
0
src/core/domain/value_objects/__init__.py
Normal file
94
src/core/domain/value_objects/email.py
Normal file
94
src/core/domain/value_objects/email.py
Normal 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)
|
||||
|
||||
202
src/core/domain/value_objects/money.py
Normal file
202
src/core/domain/value_objects/money.py
Normal 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))
|
||||
|
||||
140
src/core/domain/value_objects/phone.py
Normal file
140
src/core/domain/value_objects/phone.py
Normal 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)
|
||||
|
||||
0
src/core/utils/__init__.py
Normal file
0
src/core/utils/__init__.py
Normal file
0
src/infrastructure/__init__.py
Normal file
0
src/infrastructure/__init__.py
Normal file
0
src/infrastructure/cache/__init__.py
vendored
Normal file
0
src/infrastructure/cache/__init__.py
vendored
Normal file
0
src/infrastructure/database/__init__.py
Normal file
0
src/infrastructure/database/__init__.py
Normal file
0
src/infrastructure/database/migrations/__init__.py
Normal file
0
src/infrastructure/database/migrations/__init__.py
Normal file
87
src/infrastructure/database/migrations/env.py
Normal file
87
src/infrastructure/database/migrations/env.py
Normal 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()
|
||||
|
||||
27
src/infrastructure/database/migrations/script.py.mako
Normal file
27
src/infrastructure/database/migrations/script.py.mako
Normal 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"}
|
||||
|
||||
0
src/infrastructure/database/models/__init__.py
Normal file
0
src/infrastructure/database/models/__init__.py
Normal file
51
src/infrastructure/database/models/base.py
Normal file
51
src/infrastructure/database/models/base.py
Normal 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})>"
|
||||
|
||||
122
src/infrastructure/database/repositories/base_repository.py
Normal file
122
src/infrastructure/database/repositories/base_repository.py
Normal 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()
|
||||
|
||||
81
src/infrastructure/database/unit_of_work.py
Normal file
81
src/infrastructure/database/unit_of_work.py
Normal 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
|
||||
|
||||
0
src/infrastructure/external/__init__.py
vendored
Normal file
0
src/infrastructure/external/__init__.py
vendored
Normal file
0
src/infrastructure/external/email/__init__.py
vendored
Normal file
0
src/infrastructure/external/email/__init__.py
vendored
Normal file
0
src/infrastructure/external/payment/__init__.py
vendored
Normal file
0
src/infrastructure/external/payment/__init__.py
vendored
Normal file
0
src/infrastructure/external/providers/__init__.py
vendored
Normal file
0
src/infrastructure/external/providers/__init__.py
vendored
Normal file
0
src/infrastructure/external/sms/__init__.py
vendored
Normal file
0
src/infrastructure/external/sms/__init__.py
vendored
Normal file
0
src/infrastructure/logging/__init__.py
Normal file
0
src/infrastructure/logging/__init__.py
Normal file
0
src/infrastructure/security/__init__.py
Normal file
0
src/infrastructure/security/__init__.py
Normal file
0
src/infrastructure/tasks/__init__.py
Normal file
0
src/infrastructure/tasks/__init__.py
Normal file
0
src/presentation/__init__.py
Normal file
0
src/presentation/__init__.py
Normal file
0
src/presentation/api/__init__.py
Normal file
0
src/presentation/api/__init__.py
Normal file
0
src/presentation/api/middleware/__init__.py
Normal file
0
src/presentation/api/middleware/__init__.py
Normal file
0
src/presentation/api/routes/__init__.py
Normal file
0
src/presentation/api/routes/__init__.py
Normal file
0
src/presentation/web/__init__.py
Normal file
0
src/presentation/web/__init__.py
Normal file
31
src/presentation/web/assets/custom.css
Normal file
31
src/presentation/web/assets/custom.css
Normal 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); }
|
||||
}
|
||||
|
||||
0
src/presentation/web/components/__init__.py
Normal file
0
src/presentation/web/components/__init__.py
Normal file
0
src/presentation/web/components/cards/__init__.py
Normal file
0
src/presentation/web/components/cards/__init__.py
Normal file
0
src/presentation/web/components/common/__init__.py
Normal file
0
src/presentation/web/components/common/__init__.py
Normal file
0
src/presentation/web/components/forms/__init__.py
Normal file
0
src/presentation/web/components/forms/__init__.py
Normal file
0
src/presentation/web/pages/__init__.py
Normal file
0
src/presentation/web/pages/__init__.py
Normal file
0
src/presentation/web/pages/admin/__init__.py
Normal file
0
src/presentation/web/pages/admin/__init__.py
Normal file
0
src/presentation/web/pages/auth/__init__.py
Normal file
0
src/presentation/web/pages/auth/__init__.py
Normal file
0
src/presentation/web/pages/dashboard/__init__.py
Normal file
0
src/presentation/web/pages/dashboard/__init__.py
Normal file
0
src/presentation/web/pages/landing/__init__.py
Normal file
0
src/presentation/web/pages/landing/__init__.py
Normal file
1176
src/presentation/web/pages/landing/index.py
Normal file
1176
src/presentation/web/pages/landing/index.py
Normal file
File diff suppressed because it is too large
Load Diff
0
src/presentation/web/state/__init__.py
Normal file
0
src/presentation/web/state/__init__.py
Normal file
0
src/presentation/web/styles/__init__.py
Normal file
0
src/presentation/web/styles/__init__.py
Normal file
0
src/shared/__init__.py
Normal file
0
src/shared/__init__.py
Normal file
0
src/shared/events/__init__.py
Normal file
0
src/shared/events/__init__.py
Normal file
0
src/shared/messaging/__init__.py
Normal file
0
src/shared/messaging/__init__.py
Normal file
Reference in New Issue
Block a user