[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/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
Reference in New Issue
Block a user