# app/modules/base.py """ Base module definition class. A Module is a self-contained unit of functionality that can be enabled/disabled per platform. Each module contains: - Features: Granular capabilities for tier-based access control - Menu items: Sidebar entries per frontend type - Routes: API and page routes (future: dynamically registered) - Services: Business logic (optional, for self-contained modules) - Models: Database models (optional, for self-contained modules) - Schemas: Pydantic schemas (optional, for self-contained modules) - Templates: Jinja2 templates (optional, for self-contained modules) Self-Contained Module Structure: app/modules// ├── __init__.py ├── definition.py # ModuleDefinition instance ├── config.py # Module configuration schema (optional) ├── exceptions.py # Module-specific exceptions (optional) ├── routes/ # FastAPI routers ├── services/ # Business logic ├── models/ # SQLAlchemy models ├── schemas/ # Pydantic schemas └── templates/ # Jinja2 templates (namespaced) """ from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from fastapi import APIRouter from models.database.admin_menu_config import FrontendType @dataclass class ModuleDefinition: """ Definition of a platform module. A module groups related functionality that can be enabled/disabled per platform. Core modules cannot be disabled and are always available. Self-contained modules include their own services, models, schemas, and templates. The path attributes describe where these components are located. Attributes: code: Unique identifier (e.g., "billing", "marketplace") name: Display name (e.g., "Billing & Subscriptions") description: Description of what this module provides requires: List of module codes this module depends on features: List of feature codes this module provides menu_items: Dict mapping FrontendType to list of menu item IDs is_core: Core modules cannot be disabled admin_router: FastAPI router for admin routes vendor_router: FastAPI router for vendor routes services_path: Path to services subpackage (e.g., "app.modules.billing.services") models_path: Path to models subpackage (e.g., "app.modules.billing.models") schemas_path: Path to schemas subpackage (e.g., "app.modules.billing.schemas") templates_path: Path to templates directory (relative to module) exceptions_path: Path to exceptions module (e.g., "app.modules.billing.exceptions") locales_path: Path to locales directory (relative to module, e.g., "locales") is_self_contained: Whether module uses self-contained structure Example (traditional thin wrapper): billing_module = ModuleDefinition( code="billing", name="Billing & Subscriptions", description="Subscription tiers, billing history, and payment processing", features=["subscription_management", "billing_history", "stripe_integration"], menu_items={ FrontendType.ADMIN: ["subscription-tiers", "subscriptions", "billing-history"], FrontendType.VENDOR: ["billing"], }, ) Example (self-contained module): cms_module = ModuleDefinition( code="cms", name="Content Management", description="Content pages, media library, and vendor themes.", features=["cms_basic", "cms_custom_pages"], menu_items={ FrontendType.ADMIN: ["content-pages"], FrontendType.VENDOR: ["content-pages", "media"], }, is_self_contained=True, services_path="app.modules.cms.services", models_path="app.modules.cms.models", schemas_path="app.modules.cms.schemas", templates_path="templates", exceptions_path="app.modules.cms.exceptions", locales_path="locales", ) """ # Identity code: str name: str description: str = "" # Dependencies requires: list[str] = field(default_factory=list) # Components features: list[str] = field(default_factory=list) menu_items: dict[FrontendType, list[str]] = field(default_factory=dict) # Status is_core: bool = False # Routes (registered dynamically) admin_router: "APIRouter | None" = None vendor_router: "APIRouter | None" = None # Self-contained module paths (optional) is_self_contained: bool = False services_path: str | None = None models_path: str | None = None schemas_path: str | None = None templates_path: str | None = None # Relative to module directory exceptions_path: str | None = None locales_path: str | None = None # Relative to module directory, e.g., "locales" def get_menu_items(self, frontend_type: FrontendType) -> list[str]: """Get menu item IDs for a specific frontend type.""" return self.menu_items.get(frontend_type, []) def get_all_menu_items(self) -> set[str]: """Get all menu item IDs across all frontend types.""" all_items = set() for items in self.menu_items.values(): all_items.update(items) return all_items def has_feature(self, feature_code: str) -> bool: """Check if this module provides a specific feature.""" return feature_code in self.features def has_menu_item(self, menu_item_id: str) -> bool: """Check if this module provides a specific menu item.""" return menu_item_id in self.get_all_menu_items() def check_dependencies(self, enabled_modules: set[str]) -> list[str]: """ Check if all required modules are enabled. Args: enabled_modules: Set of enabled module codes Returns: List of missing required module codes """ return [req for req in self.requires if req not in enabled_modules] # ========================================================================= # Self-Contained Module Methods # ========================================================================= def get_module_dir(self) -> Path: """ Get the filesystem path to this module's directory. Returns: Path to app/modules// """ return Path(__file__).parent / self.code def get_templates_dir(self) -> Path | None: """ Get the filesystem path to this module's templates directory. Returns: Path to templates directory, or None if not self-contained """ if not self.is_self_contained or not self.templates_path: return None return self.get_module_dir() / self.templates_path def get_locales_dir(self) -> Path | None: """ Get the filesystem path to this module's locales directory. Returns: Path to locales directory, or None if not configured """ if not self.is_self_contained or not self.locales_path: return None return self.get_module_dir() / self.locales_path def get_import_path(self, component: str) -> str | None: """ Get the Python import path for a module component. Args: component: One of "services", "models", "schemas", "exceptions" Returns: Import path string, or None if not configured """ paths = { "services": self.services_path, "models": self.models_path, "schemas": self.schemas_path, "exceptions": self.exceptions_path, } return paths.get(component) def validate_structure(self) -> list[str]: """ Validate that self-contained module has expected directory structure. Returns: List of missing or invalid paths (empty if valid) """ if not self.is_self_contained: return [] issues = [] module_dir = self.get_module_dir() if not module_dir.exists(): issues.append(f"Module directory not found: {module_dir}") return issues # Check optional directories expected_dirs = [] if self.services_path: expected_dirs.append("services") if self.models_path: expected_dirs.append("models") if self.schemas_path: expected_dirs.append("schemas") if self.templates_path: expected_dirs.append(self.templates_path) if self.locales_path: expected_dirs.append(self.locales_path) for dir_name in expected_dirs: dir_path = module_dir / dir_name if not dir_path.exists(): issues.append(f"Missing directory: {dir_path}") return issues def __hash__(self) -> int: return hash(self.code) def __eq__(self, other: object) -> bool: if isinstance(other, ModuleDefinition): return self.code == other.code return False def __repr__(self) -> str: sc = ", self_contained" if self.is_self_contained else "" return f""