feat: add module config and migrations auto-discovery infrastructure
Add self-contained configuration and migrations support for modules:
Config auto-discovery (app/modules/config.py):
- Modules can have config.py with Pydantic Settings
- Environment variables prefixed with MODULE_NAME_
- Auto-discovered via get_module_config()
Migrations auto-discovery:
- Each module has migrations/versions/ directory
- Alembic discovers module migrations automatically
- Naming convention: {module}_{seq}_{description}.py
New architecture rules (MOD-013 to MOD-015):
- MOD-013: config.py should export config/config_class
- MOD-014: Migrations must follow naming convention
- MOD-015: Migrations directory must have __init__.py
Created for all 11 self-contained modules:
- config.py placeholder files
- migrations/ directories with __init__.py files
Added core and tenancy module definitions for completeness.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
421
.architecture-rules/module.yaml
Normal file
421
.architecture-rules/module.yaml
Normal file
@@ -0,0 +1,421 @@
|
||||
# Architecture Rules - Module Structure Rules
|
||||
# Rules for app/modules/*/ directories
|
||||
|
||||
module_rules:
|
||||
|
||||
- id: "MOD-001"
|
||||
name: "Self-contained modules must have required directories"
|
||||
severity: "error"
|
||||
description: |
|
||||
When a module declares is_self_contained=True in its definition,
|
||||
it must have the following directory structure:
|
||||
|
||||
Required directories:
|
||||
- services/ - Business logic services (actual code, not re-exports)
|
||||
- models/ - Database models (actual code, not re-exports)
|
||||
- schemas/ - Pydantic schemas (actual code, not re-exports)
|
||||
- routes/ - API and page routes
|
||||
- routes/api/ - API endpoints
|
||||
- routes/pages/ - Page endpoints (if module has UI)
|
||||
|
||||
Optional directories (based on module needs):
|
||||
- templates/ - Jinja templates (if module has UI)
|
||||
- static/ - Static assets (JS/CSS)
|
||||
- locales/ - Translation files
|
||||
- tasks/ - Celery tasks
|
||||
- exceptions.py - Module-specific exceptions
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Consistency: All modules follow the same structure
|
||||
- Discoverability: Developers know where to find code
|
||||
- Encapsulation: Module owns all its components
|
||||
- Testability: Clear boundaries for unit tests
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/"
|
||||
required_when: "is_self_contained=True"
|
||||
required_dirs:
|
||||
- "services/"
|
||||
- "models/"
|
||||
- "schemas/"
|
||||
- "routes/"
|
||||
- "routes/api/"
|
||||
|
||||
- id: "MOD-002"
|
||||
name: "Module services must contain actual code, not re-exports"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Self-contained module services should contain actual business logic,
|
||||
not just re-exports from legacy locations.
|
||||
|
||||
If a module's services/ directory only contains re-exports (from ...),
|
||||
the module is not truly self-contained.
|
||||
|
||||
WRONG (re-export only):
|
||||
# app/modules/analytics/services/stats_service.py
|
||||
from app.services.stats_service import stats_service, StatsService
|
||||
__all__ = ["stats_service", "StatsService"]
|
||||
|
||||
RIGHT (actual code):
|
||||
# app/modules/analytics/services/stats_service.py
|
||||
class StatsService:
|
||||
def get_stats(self, db: Session, vendor_id: int):
|
||||
# Actual implementation here
|
||||
...
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/services/*.py"
|
||||
anti_patterns:
|
||||
- "^from app\\.services\\."
|
||||
- "^from app\\/services\\/"
|
||||
|
||||
- id: "MOD-003"
|
||||
name: "Module schemas must contain actual code, not re-exports"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Self-contained module schemas should contain actual Pydantic models,
|
||||
not just re-exports from legacy models/schema/ location.
|
||||
|
||||
WRONG (re-export only):
|
||||
# app/modules/analytics/schemas/stats.py
|
||||
from models.schema.stats import StatsResponse, ...
|
||||
__all__ = ["StatsResponse", ...]
|
||||
|
||||
RIGHT (actual code):
|
||||
# app/modules/analytics/schemas/stats.py
|
||||
class StatsResponse(BaseModel):
|
||||
total: int
|
||||
pending: int
|
||||
...
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/schemas/*.py"
|
||||
anti_patterns:
|
||||
- "^from models\\.schema\\."
|
||||
- "^from models\\/schema\\/"
|
||||
|
||||
- id: "MOD-004"
|
||||
name: "Module routes must use module-internal implementations"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Module routes should import services from within the module,
|
||||
not from legacy app/services/ locations.
|
||||
|
||||
WRONG:
|
||||
from app.services.stats_service import stats_service
|
||||
|
||||
RIGHT:
|
||||
from app.modules.analytics.services import stats_service
|
||||
# or
|
||||
from ..services import stats_service
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/routes/**/*.py"
|
||||
anti_patterns:
|
||||
- "from app\\.services\\."
|
||||
|
||||
- id: "MOD-005"
|
||||
name: "Modules with UI must have templates and static directories"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Modules that define menu_items (have UI pages) should have:
|
||||
- templates/{module_code}/admin/ or templates/{module_code}/vendor/
|
||||
- static/admin/js/ or static/vendor/js/
|
||||
|
||||
This ensures:
|
||||
- UI is self-contained within the module
|
||||
- Templates are namespaced to avoid conflicts
|
||||
- JavaScript can be module-specific
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/"
|
||||
required_when: "has_menu_items=True"
|
||||
required_dirs:
|
||||
- "templates/"
|
||||
- "static/"
|
||||
|
||||
- id: "MOD-006"
|
||||
name: "Module locales should exist for internationalization"
|
||||
severity: "info"
|
||||
description: |
|
||||
Self-contained modules should have a locales/ directory with
|
||||
translation files for internationalization.
|
||||
|
||||
Structure:
|
||||
app/modules/{module}/locales/
|
||||
en.json
|
||||
de.json
|
||||
fr.json
|
||||
lu.json
|
||||
|
||||
Translation keys are namespaced as {module}.key_name
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/"
|
||||
suggested_dirs:
|
||||
- "locales/"
|
||||
|
||||
- id: "MOD-008"
|
||||
name: "Self-contained modules must have exceptions.py"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Self-contained modules should have an exceptions.py file defining
|
||||
module-specific exceptions that inherit from WizamartException.
|
||||
|
||||
Structure:
|
||||
app/modules/{module}/exceptions.py
|
||||
|
||||
Example:
|
||||
# app/modules/analytics/exceptions.py
|
||||
from app.exceptions import WizamartException
|
||||
|
||||
class AnalyticsException(WizamartException):
|
||||
"""Base exception for analytics module."""
|
||||
pass
|
||||
|
||||
class ReportGenerationError(AnalyticsException):
|
||||
"""Error generating analytics report."""
|
||||
pass
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Encapsulation: Module owns its exception hierarchy
|
||||
- Clarity: Clear which exceptions belong to which module
|
||||
- Testability: Easy to mock module-specific exceptions
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/"
|
||||
required_when: "is_self_contained=True"
|
||||
required_files:
|
||||
- "exceptions.py"
|
||||
|
||||
# =========================================================================
|
||||
# Auto-Discovery Rules
|
||||
# =========================================================================
|
||||
|
||||
- id: "MOD-009"
|
||||
name: "Module must have definition.py for auto-discovery"
|
||||
severity: "error"
|
||||
description: |
|
||||
Every module directory must have a definition.py file containing
|
||||
a ModuleDefinition instance for auto-discovery.
|
||||
|
||||
The framework auto-discovers modules from app/modules/*/definition.py.
|
||||
Without this file, the module won't be registered.
|
||||
|
||||
Required:
|
||||
app/modules/<code>/definition.py
|
||||
|
||||
The definition.py must export a ModuleDefinition instance:
|
||||
# app/modules/analytics/definition.py
|
||||
from app.modules.base import ModuleDefinition
|
||||
|
||||
analytics_module = ModuleDefinition(
|
||||
code="analytics",
|
||||
name="Analytics & Reporting",
|
||||
...
|
||||
)
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/"
|
||||
required_files:
|
||||
- "definition.py"
|
||||
- "__init__.py"
|
||||
|
||||
- id: "MOD-010"
|
||||
name: "Module routes must export router variable for auto-discovery"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Route files (admin.py, vendor.py, shop.py) in routes/api/ and routes/pages/
|
||||
must export a 'router' variable for auto-discovery.
|
||||
|
||||
The route discovery system looks for:
|
||||
- routes/api/admin.py with 'router' variable
|
||||
- routes/api/vendor.py with 'router' variable
|
||||
- routes/pages/vendor.py with 'router' variable
|
||||
|
||||
Example:
|
||||
# app/modules/analytics/routes/pages/vendor.py
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter() # Must be named 'router'
|
||||
|
||||
@router.get("/analytics")
|
||||
def analytics_page():
|
||||
...
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/routes/*/*.py"
|
||||
required_exports:
|
||||
- "router"
|
||||
|
||||
- id: "MOD-011"
|
||||
name: "Module tasks must have __init__.py for Celery discovery"
|
||||
severity: "warning"
|
||||
description: |
|
||||
If a module has a tasks/ directory, it must have __init__.py
|
||||
for Celery task auto-discovery to work.
|
||||
|
||||
Celery uses autodiscover_tasks() which requires the tasks
|
||||
package to be importable.
|
||||
|
||||
Required structure:
|
||||
app/modules/<code>/tasks/
|
||||
├── __init__.py <- Required for discovery
|
||||
└── some_task.py
|
||||
|
||||
The __init__.py should import task functions:
|
||||
# app/modules/billing/tasks/__init__.py
|
||||
from app.modules.billing.tasks.subscription import reset_period_counters
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/tasks/"
|
||||
required_files:
|
||||
- "__init__.py"
|
||||
|
||||
- id: "MOD-012"
|
||||
name: "Module locales should have all supported language files"
|
||||
severity: "info"
|
||||
description: |
|
||||
Module locales/ directory should have translation files for
|
||||
all supported languages to ensure consistent i18n.
|
||||
|
||||
Supported languages: en, de, fr, lu
|
||||
|
||||
Structure:
|
||||
app/modules/<code>/locales/
|
||||
├── en.json
|
||||
├── de.json
|
||||
├── fr.json
|
||||
└── lu.json
|
||||
|
||||
Missing translations will fall back to English, but it's
|
||||
better to have all languages covered.
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/locales/"
|
||||
suggested_files:
|
||||
- "en.json"
|
||||
- "de.json"
|
||||
- "fr.json"
|
||||
- "lu.json"
|
||||
|
||||
- id: "MOD-007"
|
||||
name: "Module definition must match directory structure"
|
||||
severity: "error"
|
||||
description: |
|
||||
If a module definition specifies paths like services_path, models_path,
|
||||
etc., those directories must exist and contain __init__.py files.
|
||||
|
||||
Example: If definition.py has:
|
||||
services_path="app.modules.analytics.services"
|
||||
|
||||
Then these must exist:
|
||||
app/modules/analytics/services/
|
||||
app/modules/analytics/services/__init__.py
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/definition.py"
|
||||
validates:
|
||||
- "services_path -> services/__init__.py"
|
||||
- "models_path -> models/__init__.py"
|
||||
- "schemas_path -> schemas/__init__.py"
|
||||
- "exceptions_path -> exceptions.py or exceptions/__init__.py"
|
||||
- "templates_path -> templates/"
|
||||
- "locales_path -> locales/"
|
||||
|
||||
# =========================================================================
|
||||
# Self-Contained Config & Migrations Rules
|
||||
# =========================================================================
|
||||
|
||||
- id: "MOD-013"
|
||||
name: "Module config.py for environment-based configuration"
|
||||
severity: "info"
|
||||
description: |
|
||||
Self-contained modules can have a config.py file for environment-based
|
||||
configuration using Pydantic Settings.
|
||||
|
||||
Structure:
|
||||
app/modules/<code>/config.py
|
||||
|
||||
Pattern:
|
||||
# app/modules/marketplace/config.py
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class MarketplaceConfig(BaseSettings):
|
||||
'''Configuration for marketplace module.'''
|
||||
|
||||
# Settings prefixed with MARKETPLACE_ in environment
|
||||
api_timeout: int = Field(default=30, description="API timeout")
|
||||
batch_size: int = Field(default=100, description="Import batch size")
|
||||
|
||||
model_config = {"env_prefix": "MARKETPLACE_"}
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = MarketplaceConfig
|
||||
config = MarketplaceConfig()
|
||||
|
||||
The config is auto-discovered by app/modules/config.py and can be
|
||||
accessed via get_module_config("marketplace").
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Encapsulation: Module owns its configuration
|
||||
- Environment: Settings loaded from environment variables
|
||||
- Validation: Pydantic validates configuration on startup
|
||||
- Defaults: Sensible defaults in code, overridable via env
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/"
|
||||
suggested_files:
|
||||
- "config.py"
|
||||
|
||||
- id: "MOD-014"
|
||||
name: "Module migrations must follow naming convention"
|
||||
severity: "warning"
|
||||
description: |
|
||||
If a module has migrations, they must:
|
||||
1. Be located in migrations/versions/ directory
|
||||
2. Follow the naming convention: {module_code}_{sequence}_{description}.py
|
||||
|
||||
Structure:
|
||||
app/modules/<code>/migrations/
|
||||
├── __init__.py
|
||||
└── versions/
|
||||
├── __init__.py
|
||||
├── <code>_001_initial.py
|
||||
├── <code>_002_add_feature.py
|
||||
└── ...
|
||||
|
||||
Example for cms module:
|
||||
app/modules/cms/migrations/versions/
|
||||
├── cms_001_create_content_pages.py
|
||||
├── cms_002_add_sections.py
|
||||
└── cms_003_add_media_library.py
|
||||
|
||||
Migration file must include:
|
||||
revision = "cms_001"
|
||||
down_revision = None # or previous revision
|
||||
branch_labels = ("cms",) # Module-specific branch
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Isolation: Module migrations don't conflict with core
|
||||
- Ordering: Sequence numbers ensure correct order
|
||||
- Traceability: Clear which module owns each migration
|
||||
- Rollback: Can rollback module migrations independently
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/migrations/versions/"
|
||||
file_pattern: "{module_code}_*.py"
|
||||
|
||||
- id: "MOD-015"
|
||||
name: "Module migrations directory must have __init__.py files"
|
||||
severity: "warning"
|
||||
description: |
|
||||
If a module has a migrations/ directory, both the migrations/
|
||||
and migrations/versions/ directories must have __init__.py files
|
||||
for proper Python package discovery.
|
||||
|
||||
Required structure:
|
||||
app/modules/<code>/migrations/
|
||||
├── __init__.py <- Required
|
||||
└── versions/
|
||||
└── __init__.py <- Required
|
||||
|
||||
The __init__.py files can be empty or contain docstrings:
|
||||
# migrations/__init__.py
|
||||
'''Module migrations.'''
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Alembic needs to import migration scripts as Python modules
|
||||
- Without __init__.py, the directories are not Python packages
|
||||
pattern:
|
||||
directory_pattern: "app/modules/*/migrations/"
|
||||
required_files:
|
||||
- "__init__.py"
|
||||
- "versions/__init__.py"
|
||||
31
app/modules/analytics/config.py
Normal file
31
app/modules/analytics/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/analytics/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with ANALYTICS_ prefix.
|
||||
|
||||
Example:
|
||||
ANALYTICS_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.analytics.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for analytics module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "ANALYTICS_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/analytics/migrations/__init__.py
Normal file
1
app/modules/analytics/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/analytics/migrations/versions/__init__.py
Normal file
1
app/modules/analytics/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
31
app/modules/billing/config.py
Normal file
31
app/modules/billing/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/billing/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with BILLING_ prefix.
|
||||
|
||||
Example:
|
||||
BILLING_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.billing.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for billing module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "BILLING_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/billing/migrations/__init__.py
Normal file
1
app/modules/billing/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/billing/migrations/versions/__init__.py
Normal file
1
app/modules/billing/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
31
app/modules/cms/config.py
Normal file
31
app/modules/cms/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/cms/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with CMS_ prefix.
|
||||
|
||||
Example:
|
||||
CMS_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.cms.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for cms module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "CMS_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/cms/migrations/__init__.py
Normal file
1
app/modules/cms/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/cms/migrations/versions/__init__.py
Normal file
1
app/modules/cms/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
272
app/modules/config.py
Normal file
272
app/modules/config.py
Normal file
@@ -0,0 +1,272 @@
|
||||
# app/modules/config.py
|
||||
"""
|
||||
Module configuration auto-discovery utility.
|
||||
|
||||
Provides utilities for discovering and loading module-specific configurations.
|
||||
Each self-contained module can have its own config.py file that defines:
|
||||
- A Pydantic Settings class for environment-based configuration
|
||||
- Default values for module settings
|
||||
- Validation rules for configuration
|
||||
|
||||
Module Config Structure:
|
||||
app/modules/<code>/
|
||||
└── config.py # ModuleConfig class
|
||||
|
||||
Config File Pattern:
|
||||
# app/modules/mymodule/config.py
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class MyModuleConfig(BaseSettings):
|
||||
'''Configuration for mymodule.'''
|
||||
|
||||
# Module-specific settings (auto-prefixed with module code)
|
||||
api_timeout: int = Field(default=30, description="API timeout in seconds")
|
||||
max_retries: int = Field(default=3, description="Max retry attempts")
|
||||
|
||||
model_config = {"env_prefix": "MYMODULE_"}
|
||||
|
||||
# Export the config class and instance
|
||||
config_class = MyModuleConfig
|
||||
config = MyModuleConfig()
|
||||
|
||||
Usage:
|
||||
# Get all module configurations
|
||||
from app.modules.config import get_all_module_configs, get_module_config
|
||||
|
||||
configs = get_all_module_configs()
|
||||
# Returns: {"billing": <BillingConfig>, "marketplace": <MarketplaceConfig>, ...}
|
||||
|
||||
billing_config = get_module_config("billing")
|
||||
# Returns: <BillingConfig instance>
|
||||
|
||||
Integration with ModuleDefinition:
|
||||
The config auto-discovery works alongside ModuleDefinition.config_schema.
|
||||
- If config.py exists, it's auto-discovered and loaded
|
||||
- If config_schema is defined in definition.py, it takes precedence
|
||||
- Both can be used together for runtime vs static configuration
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Path to the modules directory
|
||||
MODULES_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def discover_module_config(module_code: str) -> "BaseSettings | None":
|
||||
"""
|
||||
Discover and load configuration for a specific module.
|
||||
|
||||
Looks for a config.py file in the module directory with either:
|
||||
- A 'config' variable (instantiated config)
|
||||
- A 'config_class' variable (config class to instantiate)
|
||||
|
||||
Args:
|
||||
module_code: Module code (e.g., "billing", "marketplace")
|
||||
|
||||
Returns:
|
||||
Instantiated config object, or None if no config found
|
||||
"""
|
||||
# Convert module code to directory name (e.g., dev-tools -> dev_tools)
|
||||
dir_name = module_code.replace("-", "_")
|
||||
config_file = MODULES_DIR / dir_name / "config.py"
|
||||
|
||||
if not config_file.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
import_path = f"app.modules.{dir_name}.config"
|
||||
config_module = importlib.import_module(import_path)
|
||||
|
||||
# First, try to get an instantiated config
|
||||
if hasattr(config_module, "config"):
|
||||
config = getattr(config_module, "config")
|
||||
logger.debug(f"Loaded config instance for module {module_code}")
|
||||
return config
|
||||
|
||||
# Otherwise, try to instantiate from config_class
|
||||
if hasattr(config_module, "config_class"):
|
||||
config_class = getattr(config_module, "config_class")
|
||||
config = config_class()
|
||||
logger.debug(f"Instantiated config class for module {module_code}")
|
||||
return config
|
||||
|
||||
logger.warning(
|
||||
f"Module {module_code} has config.py but no 'config' or 'config_class' export"
|
||||
)
|
||||
return None
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import config for module {module_code}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load config for module {module_code}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def discover_all_module_configs() -> dict[str, "BaseSettings"]:
|
||||
"""
|
||||
Discover and load configurations for all modules.
|
||||
|
||||
Returns:
|
||||
Dict mapping module code to config instance
|
||||
"""
|
||||
configs: dict[str, "BaseSettings"] = {}
|
||||
|
||||
for module_dir in sorted(MODULES_DIR.iterdir()):
|
||||
if not module_dir.is_dir():
|
||||
continue
|
||||
|
||||
# Skip special directories
|
||||
if module_dir.name.startswith("_"):
|
||||
continue
|
||||
|
||||
config_file = module_dir / "config.py"
|
||||
if not config_file.exists():
|
||||
continue
|
||||
|
||||
# Convert directory name to module code (e.g., dev_tools -> dev-tools)
|
||||
module_code = module_dir.name.replace("_", "-")
|
||||
config = discover_module_config(module_code)
|
||||
|
||||
if config is not None:
|
||||
configs[module_code] = config
|
||||
|
||||
logger.info(f"Discovered configs for {len(configs)} modules")
|
||||
return configs
|
||||
|
||||
|
||||
def get_all_module_configs() -> dict[str, "BaseSettings"]:
|
||||
"""
|
||||
Get all module configurations.
|
||||
|
||||
Returns:
|
||||
Dict mapping module code to config instance
|
||||
"""
|
||||
return discover_all_module_configs()
|
||||
|
||||
|
||||
def get_module_config(module_code: str) -> "BaseSettings | None":
|
||||
"""
|
||||
Get configuration for a specific module.
|
||||
|
||||
Args:
|
||||
module_code: Module code (e.g., "billing")
|
||||
|
||||
Returns:
|
||||
Config instance, or None if no config exists
|
||||
"""
|
||||
configs = get_all_module_configs()
|
||||
return configs.get(module_code)
|
||||
|
||||
|
||||
def reload_module_configs() -> dict[str, "BaseSettings"]:
|
||||
"""
|
||||
Reload all module configurations (clears cache).
|
||||
|
||||
Useful for testing or when configuration files change.
|
||||
|
||||
Returns:
|
||||
Fresh dict of module configurations
|
||||
"""
|
||||
discover_all_module_configs.cache_clear()
|
||||
return discover_all_module_configs()
|
||||
|
||||
|
||||
def get_merged_config(module_code: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get merged configuration for a module.
|
||||
|
||||
Merges configuration from multiple sources:
|
||||
1. config.py environment settings (lowest priority)
|
||||
2. ModuleDefinition.default_config
|
||||
3. ModuleDefinition.config_schema validation
|
||||
|
||||
Args:
|
||||
module_code: Module code
|
||||
|
||||
Returns:
|
||||
Merged configuration dict
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from app.modules.registry import get_module
|
||||
|
||||
config_dict: dict[str, Any] = {}
|
||||
|
||||
# 1. Load from config.py if exists
|
||||
config_instance = get_module_config(module_code)
|
||||
if config_instance is not None:
|
||||
config_dict.update(config_instance.model_dump())
|
||||
|
||||
# 2. Merge with ModuleDefinition defaults
|
||||
module = get_module(module_code)
|
||||
if module:
|
||||
# default_config is merged, overriding config.py values
|
||||
if module.default_config:
|
||||
for key, value in module.default_config.items():
|
||||
if key not in config_dict:
|
||||
config_dict[key] = value
|
||||
|
||||
# 3. Validate against config_schema if defined
|
||||
if module.config_schema:
|
||||
try:
|
||||
validated = module.config_schema(**config_dict)
|
||||
config_dict = validated.model_dump()
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Config validation failed for module {module_code}: {e}"
|
||||
)
|
||||
|
||||
return config_dict
|
||||
|
||||
|
||||
def validate_module_configs() -> list[str]:
|
||||
"""
|
||||
Validate all module configurations.
|
||||
|
||||
Returns:
|
||||
List of validation errors (empty if all valid)
|
||||
"""
|
||||
errors: list[str] = []
|
||||
|
||||
for module_dir in MODULES_DIR.iterdir():
|
||||
if not module_dir.is_dir() or module_dir.name.startswith("_"):
|
||||
continue
|
||||
|
||||
config_file = module_dir / "config.py"
|
||||
if not config_file.exists():
|
||||
continue
|
||||
|
||||
module_code = module_dir.name.replace("_", "-")
|
||||
|
||||
try:
|
||||
config = discover_module_config(module_code)
|
||||
if config is None:
|
||||
errors.append(
|
||||
f"Module {module_code}: config.py exists but no config exported"
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(f"Module {module_code}: config.py failed to load: {e}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
__all__ = [
|
||||
"discover_module_config",
|
||||
"discover_all_module_configs",
|
||||
"get_all_module_configs",
|
||||
"get_module_config",
|
||||
"reload_module_configs",
|
||||
"get_merged_config",
|
||||
"validate_module_configs",
|
||||
]
|
||||
11
app/modules/core/__init__.py
Normal file
11
app/modules/core/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# app/modules/core/__init__.py
|
||||
"""
|
||||
Core Platform module.
|
||||
|
||||
Provides dashboard, settings, and profile management.
|
||||
This is a core module that cannot be disabled.
|
||||
"""
|
||||
|
||||
from app.modules.core.definition import core_module
|
||||
|
||||
__all__ = ["core_module"]
|
||||
39
app/modules/core/definition.py
Normal file
39
app/modules/core/definition.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# app/modules/core/definition.py
|
||||
"""
|
||||
Core Platform module definition.
|
||||
|
||||
Dashboard, settings, and profile management.
|
||||
Required for basic operation - cannot be disabled.
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
core_module = ModuleDefinition(
|
||||
code="core",
|
||||
name="Core Platform",
|
||||
description="Dashboard, settings, and profile management. Required for basic operation.",
|
||||
version="1.0.0",
|
||||
is_core=True,
|
||||
features=[
|
||||
"dashboard",
|
||||
"settings",
|
||||
"profile",
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"dashboard",
|
||||
"settings",
|
||||
"email-templates",
|
||||
"my-menu",
|
||||
],
|
||||
FrontendType.VENDOR: [
|
||||
"dashboard",
|
||||
"profile",
|
||||
"settings",
|
||||
"email-templates",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
__all__ = ["core_module"]
|
||||
31
app/modules/customers/config.py
Normal file
31
app/modules/customers/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/customers/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with CUSTOMERS_ prefix.
|
||||
|
||||
Example:
|
||||
CUSTOMERS_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.customers.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for customers module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "CUSTOMERS_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/customers/migrations/__init__.py
Normal file
1
app/modules/customers/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/customers/migrations/versions/__init__.py
Normal file
1
app/modules/customers/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
31
app/modules/dev_tools/config.py
Normal file
31
app/modules/dev_tools/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/dev_tools/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with DEV_TOOLS_ prefix.
|
||||
|
||||
Example:
|
||||
DEV_TOOLS_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.dev_tools.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for dev_tools module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "DEV_TOOLS_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/dev_tools/migrations/__init__.py
Normal file
1
app/modules/dev_tools/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/dev_tools/migrations/versions/__init__.py
Normal file
1
app/modules/dev_tools/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
190
app/modules/discovery.py
Normal file
190
app/modules/discovery.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# app/modules/discovery.py
|
||||
"""
|
||||
Module auto-discovery system.
|
||||
|
||||
Provides automatic discovery of modules from app/modules/*/definition.py.
|
||||
This enables true plug-and-play modules - just create a module directory
|
||||
with a definition.py file and it will be automatically registered.
|
||||
|
||||
Module Discovery Rules:
|
||||
1. Module must have app/modules/<code>/definition.py
|
||||
2. definition.py must define a ModuleDefinition instance
|
||||
3. The instance variable name should be <code>_module (e.g., cms_module)
|
||||
4. Module tier is determined by is_core and is_internal flags
|
||||
|
||||
Usage:
|
||||
# Auto-discover all modules (replaces manual imports in registry.py)
|
||||
from app.modules.discovery import discover_modules
|
||||
|
||||
MODULES = discover_modules()
|
||||
|
||||
Directory Structure for Auto-Discovery:
|
||||
app/modules/
|
||||
├── cms/
|
||||
│ ├── __init__.py
|
||||
│ ├── definition.py <- Must export ModuleDefinition
|
||||
│ ├── routes/
|
||||
│ │ ├── api/
|
||||
│ │ └── pages/
|
||||
│ ├── services/
|
||||
│ ├── models/
|
||||
│ ├── schemas/
|
||||
│ ├── templates/
|
||||
│ ├── static/
|
||||
│ ├── locales/
|
||||
│ └── tasks/
|
||||
└── analytics/
|
||||
└── ...
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.modules.base import ModuleDefinition
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Modules directory
|
||||
MODULES_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def discover_modules() -> dict[str, "ModuleDefinition"]:
|
||||
"""
|
||||
Auto-discover all modules from app/modules/*/definition.py.
|
||||
|
||||
Scans the modules directory for subdirectories containing definition.py,
|
||||
imports them, and extracts ModuleDefinition instances.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping module code to ModuleDefinition
|
||||
|
||||
Example:
|
||||
>>> modules = discover_modules()
|
||||
>>> modules.keys()
|
||||
dict_keys(['cms', 'analytics', 'billing', ...])
|
||||
"""
|
||||
from app.modules.base import ModuleDefinition
|
||||
|
||||
modules: dict[str, ModuleDefinition] = {}
|
||||
|
||||
if not MODULES_DIR.exists():
|
||||
logger.warning(f"Modules directory not found: {MODULES_DIR}")
|
||||
return modules
|
||||
|
||||
for module_dir in sorted(MODULES_DIR.iterdir()):
|
||||
# Skip non-directories and special files
|
||||
if not module_dir.is_dir():
|
||||
continue
|
||||
if module_dir.name.startswith("_") or module_dir.name.startswith("."):
|
||||
continue
|
||||
|
||||
definition_file = module_dir / "definition.py"
|
||||
if not definition_file.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
# Import the definition module
|
||||
dir_name = module_dir.name
|
||||
import_path = f"app.modules.{dir_name}.definition"
|
||||
definition_module = importlib.import_module(import_path)
|
||||
|
||||
# Find ModuleDefinition instances in the module
|
||||
for attr_name in dir(definition_module):
|
||||
if attr_name.startswith("_"):
|
||||
continue
|
||||
|
||||
attr = getattr(definition_module, attr_name)
|
||||
if isinstance(attr, ModuleDefinition):
|
||||
modules[attr.code] = attr
|
||||
logger.debug(f"Discovered module: {attr.code} from {import_path}")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"No ModuleDefinition found in {import_path}")
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import {import_path}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error discovering module in {module_dir}: {e}")
|
||||
|
||||
logger.info(f"Auto-discovered {len(modules)} modules")
|
||||
return modules
|
||||
|
||||
|
||||
def discover_modules_by_tier() -> dict[str, dict[str, "ModuleDefinition"]]:
|
||||
"""
|
||||
Discover modules and organize by tier.
|
||||
|
||||
Returns:
|
||||
Dict with keys 'core', 'optional', 'internal' mapping to module dicts
|
||||
|
||||
Example:
|
||||
>>> by_tier = discover_modules_by_tier()
|
||||
>>> by_tier['core'].keys()
|
||||
dict_keys(['core', 'tenancy', 'cms', 'customers'])
|
||||
"""
|
||||
modules = discover_modules()
|
||||
|
||||
result = {
|
||||
"core": {},
|
||||
"optional": {},
|
||||
"internal": {},
|
||||
}
|
||||
|
||||
for code, module in modules.items():
|
||||
tier = module.get_tier()
|
||||
result[tier][code] = module
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_module_codes() -> set[str]:
|
||||
"""Get all discovered module codes."""
|
||||
return set(discover_modules().keys())
|
||||
|
||||
|
||||
def validate_module_discovery() -> list[str]:
|
||||
"""
|
||||
Validate that all module directories have proper structure.
|
||||
|
||||
Returns:
|
||||
List of validation errors (empty if all valid)
|
||||
"""
|
||||
errors: list[str] = []
|
||||
|
||||
if not MODULES_DIR.exists():
|
||||
return ["Modules directory not found"]
|
||||
|
||||
for module_dir in sorted(MODULES_DIR.iterdir()):
|
||||
if not module_dir.is_dir():
|
||||
continue
|
||||
if module_dir.name.startswith("_") or module_dir.name.startswith("."):
|
||||
continue
|
||||
|
||||
# Check for definition.py
|
||||
definition_file = module_dir / "definition.py"
|
||||
if not definition_file.exists():
|
||||
# Only warn if it looks like a module (has __init__.py)
|
||||
if (module_dir / "__init__.py").exists():
|
||||
errors.append(
|
||||
f"Module directory '{module_dir.name}' has __init__.py but no definition.py"
|
||||
)
|
||||
continue
|
||||
|
||||
# Check for __init__.py
|
||||
init_file = module_dir / "__init__.py"
|
||||
if not init_file.exists():
|
||||
errors.append(f"Module '{module_dir.name}' missing __init__.py")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
__all__ = [
|
||||
"discover_modules",
|
||||
"discover_modules_by_tier",
|
||||
"get_module_codes",
|
||||
"validate_module_discovery",
|
||||
"MODULES_DIR",
|
||||
]
|
||||
31
app/modules/inventory/config.py
Normal file
31
app/modules/inventory/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/inventory/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with INVENTORY_ prefix.
|
||||
|
||||
Example:
|
||||
INVENTORY_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.inventory.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for inventory module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "INVENTORY_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/inventory/migrations/__init__.py
Normal file
1
app/modules/inventory/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/inventory/migrations/versions/__init__.py
Normal file
1
app/modules/inventory/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
31
app/modules/marketplace/config.py
Normal file
31
app/modules/marketplace/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/marketplace/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with MARKETPLACE_ prefix.
|
||||
|
||||
Example:
|
||||
MARKETPLACE_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.marketplace.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for marketplace module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "MARKETPLACE_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/marketplace/migrations/__init__.py
Normal file
1
app/modules/marketplace/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/marketplace/migrations/versions/__init__.py
Normal file
1
app/modules/marketplace/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
31
app/modules/messaging/config.py
Normal file
31
app/modules/messaging/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/messaging/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with MESSAGING_ prefix.
|
||||
|
||||
Example:
|
||||
MESSAGING_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.messaging.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for messaging module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "MESSAGING_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/messaging/migrations/__init__.py
Normal file
1
app/modules/messaging/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/messaging/migrations/versions/__init__.py
Normal file
1
app/modules/messaging/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
31
app/modules/monitoring/config.py
Normal file
31
app/modules/monitoring/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/monitoring/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with MONITORING_ prefix.
|
||||
|
||||
Example:
|
||||
MONITORING_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.monitoring.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for monitoring module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "MONITORING_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/monitoring/migrations/__init__.py
Normal file
1
app/modules/monitoring/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/monitoring/migrations/versions/__init__.py
Normal file
1
app/modules/monitoring/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
31
app/modules/orders/config.py
Normal file
31
app/modules/orders/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/orders/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with ORDERS_ prefix.
|
||||
|
||||
Example:
|
||||
ORDERS_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.orders.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for orders module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "ORDERS_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/orders/migrations/__init__.py
Normal file
1
app/modules/orders/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/orders/migrations/versions/__init__.py
Normal file
1
app/modules/orders/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
31
app/modules/payments/config.py
Normal file
31
app/modules/payments/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# app/modules/payments/config.py
|
||||
"""
|
||||
Module configuration.
|
||||
|
||||
Environment-based configuration using Pydantic Settings.
|
||||
Settings are loaded from environment variables with PAYMENTS_ prefix.
|
||||
|
||||
Example:
|
||||
PAYMENTS_SETTING_NAME=value
|
||||
|
||||
Usage:
|
||||
from app.modules.payments.config import config
|
||||
value = config.setting_name
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ModuleConfig(BaseSettings):
|
||||
"""Configuration for payments module."""
|
||||
|
||||
# Add module-specific settings here
|
||||
# Example:
|
||||
# api_timeout: int = 30
|
||||
# batch_size: int = 100
|
||||
|
||||
model_config = {"env_prefix": "PAYMENTS_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = ModuleConfig
|
||||
config = ModuleConfig()
|
||||
1
app/modules/payments/migrations/__init__.py
Normal file
1
app/modules/payments/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/payments/migrations/versions/__init__.py
Normal file
1
app/modules/payments/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migration versions."""
|
||||
248
app/modules/routes.py
Normal file
248
app/modules/routes.py
Normal file
@@ -0,0 +1,248 @@
|
||||
# app/modules/routes.py
|
||||
"""
|
||||
Module route discovery for FastAPI.
|
||||
|
||||
Provides utilities to:
|
||||
- Discover route modules from all registered modules
|
||||
- Auto-register API and page routes from self-contained modules
|
||||
|
||||
This module bridges the gap between the module system and FastAPI,
|
||||
allowing routes to be defined within modules and automatically
|
||||
discovered and registered.
|
||||
|
||||
Usage:
|
||||
# In main.py
|
||||
from app.modules.routes import discover_module_routes
|
||||
|
||||
# Auto-discover and register routes
|
||||
for route_info in discover_module_routes():
|
||||
app.include_router(
|
||||
route_info["router"],
|
||||
prefix=route_info["prefix"],
|
||||
tags=route_info["tags"],
|
||||
include_in_schema=route_info.get("include_in_schema", True),
|
||||
)
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import APIRouter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouteInfo:
|
||||
"""Information about a discovered route."""
|
||||
|
||||
router: "APIRouter"
|
||||
prefix: str
|
||||
tags: list[str]
|
||||
include_in_schema: bool = True
|
||||
module_code: str = ""
|
||||
route_type: str = "" # "api" or "pages"
|
||||
frontend: str = "" # "admin", "vendor", "shop"
|
||||
|
||||
|
||||
def discover_module_routes() -> list[RouteInfo]:
|
||||
"""
|
||||
Discover routes from all registered self-contained modules.
|
||||
|
||||
Scans all modules in the registry and returns RouteInfo objects for
|
||||
modules that have routes directories.
|
||||
|
||||
Route discovery looks for:
|
||||
- routes/api/admin.py -> admin API routes
|
||||
- routes/api/vendor.py -> vendor API routes
|
||||
- routes/api/shop.py -> shop API routes
|
||||
- routes/pages/admin.py -> admin page routes
|
||||
- routes/pages/vendor.py -> vendor page routes
|
||||
|
||||
Returns:
|
||||
List of RouteInfo objects with router and registration info
|
||||
|
||||
Example:
|
||||
>>> routes = discover_module_routes()
|
||||
>>> for route in routes:
|
||||
... print(f"{route.module_code}: {route.route_type}/{route.frontend}")
|
||||
analytics: pages/vendor
|
||||
cms: api/admin
|
||||
cms: api/vendor
|
||||
cms: pages/admin
|
||||
cms: pages/vendor
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
routes: list[RouteInfo] = []
|
||||
|
||||
for module in MODULES.values():
|
||||
if not module.is_self_contained:
|
||||
continue
|
||||
|
||||
module_routes = _discover_module_routes(module.code)
|
||||
routes.extend(module_routes)
|
||||
|
||||
logger.info(f"Discovered {len(routes)} routes from self-contained modules")
|
||||
return routes
|
||||
|
||||
|
||||
def _discover_module_routes(module_code: str) -> list[RouteInfo]:
|
||||
"""
|
||||
Discover routes for a specific module.
|
||||
|
||||
Args:
|
||||
module_code: Module code (e.g., "analytics", "cms")
|
||||
|
||||
Returns:
|
||||
List of RouteInfo for this module
|
||||
"""
|
||||
routes: list[RouteInfo] = []
|
||||
dir_name = module_code.replace("-", "_")
|
||||
module_path = Path(__file__).parent / dir_name
|
||||
|
||||
if not module_path.exists():
|
||||
return routes
|
||||
|
||||
routes_path = module_path / "routes"
|
||||
if not routes_path.exists():
|
||||
return routes
|
||||
|
||||
# Discover API routes
|
||||
api_routes = _discover_routes_in_dir(
|
||||
module_code, dir_name, routes_path / "api", "api"
|
||||
)
|
||||
routes.extend(api_routes)
|
||||
|
||||
# Discover page routes
|
||||
page_routes = _discover_routes_in_dir(
|
||||
module_code, dir_name, routes_path / "pages", "pages"
|
||||
)
|
||||
routes.extend(page_routes)
|
||||
|
||||
return routes
|
||||
|
||||
|
||||
def _discover_routes_in_dir(
|
||||
module_code: str, dir_name: str, routes_dir: Path, route_type: str
|
||||
) -> list[RouteInfo]:
|
||||
"""
|
||||
Discover routes in a specific directory (api/ or pages/).
|
||||
|
||||
Args:
|
||||
module_code: Module code for tags
|
||||
dir_name: Directory name (module_code with _ instead of -)
|
||||
routes_dir: Path to routes/api/ or routes/pages/
|
||||
route_type: "api" or "pages"
|
||||
|
||||
Returns:
|
||||
List of RouteInfo for discovered routes
|
||||
"""
|
||||
routes: list[RouteInfo] = []
|
||||
|
||||
if not routes_dir.exists():
|
||||
return routes
|
||||
|
||||
# Look for admin.py, vendor.py, shop.py
|
||||
frontends = {
|
||||
"admin": {
|
||||
"api_prefix": "/api/v1/admin",
|
||||
"pages_prefix": "/admin",
|
||||
"include_in_schema": True,
|
||||
},
|
||||
"vendor": {
|
||||
"api_prefix": "/api/v1/vendor",
|
||||
"pages_prefix": "/vendor",
|
||||
"include_in_schema": True if route_type == "api" else False,
|
||||
},
|
||||
"shop": {
|
||||
"api_prefix": "/api/v1/shop",
|
||||
"pages_prefix": "/shop",
|
||||
"include_in_schema": True if route_type == "api" else False,
|
||||
},
|
||||
}
|
||||
|
||||
for frontend, config in frontends.items():
|
||||
route_file = routes_dir / f"{frontend}.py"
|
||||
if not route_file.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
# Import the module
|
||||
import_path = f"app.modules.{dir_name}.routes.{route_type}.{frontend}"
|
||||
route_module = importlib.import_module(import_path)
|
||||
|
||||
# Get the router (try common names)
|
||||
router = None
|
||||
for attr_name in ["router", f"{frontend}_router", "api_router", "page_router"]:
|
||||
if hasattr(route_module, attr_name):
|
||||
router = getattr(route_module, attr_name)
|
||||
break
|
||||
|
||||
if router is None:
|
||||
logger.warning(f"No router found in {import_path}")
|
||||
continue
|
||||
|
||||
# Determine prefix based on route type
|
||||
if route_type == "api":
|
||||
prefix = config["api_prefix"]
|
||||
else:
|
||||
prefix = config["pages_prefix"]
|
||||
|
||||
# Build tags
|
||||
tags = [f"{module_code}-{frontend}-{route_type}"]
|
||||
|
||||
route_info = RouteInfo(
|
||||
router=router,
|
||||
prefix=prefix,
|
||||
tags=tags,
|
||||
include_in_schema=config["include_in_schema"],
|
||||
module_code=module_code,
|
||||
route_type=route_type,
|
||||
frontend=frontend,
|
||||
)
|
||||
routes.append(route_info)
|
||||
|
||||
logger.debug(f"Discovered route: {module_code} {route_type}/{frontend}")
|
||||
|
||||
except ImportError as e:
|
||||
logger.warning(f"Failed to import {import_path}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error discovering routes in {route_file}: {e}")
|
||||
|
||||
return routes
|
||||
|
||||
|
||||
def get_api_routes() -> list[RouteInfo]:
|
||||
"""Get only API routes from modules."""
|
||||
return [r for r in discover_module_routes() if r.route_type == "api"]
|
||||
|
||||
|
||||
def get_page_routes() -> list[RouteInfo]:
|
||||
"""Get only page routes from modules."""
|
||||
return [r for r in discover_module_routes() if r.route_type == "pages"]
|
||||
|
||||
|
||||
def get_vendor_page_routes() -> list[RouteInfo]:
|
||||
"""Get vendor page routes from modules."""
|
||||
return [r for r in discover_module_routes() if r.route_type == "pages" and r.frontend == "vendor"]
|
||||
|
||||
|
||||
def get_admin_page_routes() -> list[RouteInfo]:
|
||||
"""Get admin page routes from modules."""
|
||||
return [r for r in discover_module_routes() if r.route_type == "pages" and r.frontend == "admin"]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"RouteInfo",
|
||||
"discover_module_routes",
|
||||
"get_api_routes",
|
||||
"get_page_routes",
|
||||
"get_vendor_page_routes",
|
||||
"get_admin_page_routes",
|
||||
]
|
||||
11
app/modules/tenancy/__init__.py
Normal file
11
app/modules/tenancy/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# app/modules/tenancy/__init__.py
|
||||
"""
|
||||
Tenancy Management module.
|
||||
|
||||
Provides platform, company, vendor, and admin user management.
|
||||
This is a core module that cannot be disabled.
|
||||
"""
|
||||
|
||||
from app.modules.tenancy.definition import tenancy_module
|
||||
|
||||
__all__ = ["tenancy_module"]
|
||||
37
app/modules/tenancy/definition.py
Normal file
37
app/modules/tenancy/definition.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# app/modules/tenancy/definition.py
|
||||
"""
|
||||
Tenancy Management module definition.
|
||||
|
||||
Platform, company, vendor, and admin user management.
|
||||
Required for multi-tenant operation - cannot be disabled.
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
tenancy_module = ModuleDefinition(
|
||||
code="tenancy",
|
||||
name="Tenancy Management",
|
||||
description="Platform, company, vendor, and admin user management. Required for multi-tenant operation.",
|
||||
version="1.0.0",
|
||||
is_core=True,
|
||||
features=[
|
||||
"platform_management",
|
||||
"company_management",
|
||||
"vendor_management",
|
||||
"admin_user_management",
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"platforms",
|
||||
"companies",
|
||||
"vendors",
|
||||
"admin-users",
|
||||
],
|
||||
FrontendType.VENDOR: [
|
||||
"team",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
__all__ = ["tenancy_module"]
|
||||
@@ -1,6 +1,13 @@
|
||||
# Module System Architecture
|
||||
|
||||
The Wizamart platform uses a modular architecture that allows features to be enabled or disabled per platform. This document explains the module system, its classification tiers, and how modules interact with the rest of the application.
|
||||
The Wizamart platform uses a **plug-and-play modular architecture** where modules are fully self-contained and automatically discovered. Simply create a module directory with the required structure, and the framework handles registration, routing, and resource loading automatically.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Auto-Discovery**: Modules are automatically discovered from `app/modules/*/definition.py`
|
||||
- **Zero Configuration**: No changes to `main.py`, `registry.py`, or other framework files needed
|
||||
- **Self-Contained**: Each module owns its routes, services, models, templates, and translations
|
||||
- **Hot-Pluggable**: Add or remove modules by simply adding/removing directories
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -14,7 +21,7 @@ The Wizamart platform uses a modular architecture that allows features to be ena
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ MODULE LAYER │
|
||||
│ AUTO-DISCOVERED MODULE LAYER │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ CORE MODULES (Always Enabled) │ │
|
||||
@@ -33,6 +40,36 @@ The Wizamart platform uses a modular architecture that allows features to be ena
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Auto-Discovery System
|
||||
|
||||
All module components are automatically discovered by the framework:
|
||||
|
||||
| Component | Discovery Location | Auto-Loaded By |
|
||||
|-----------|-------------------|----------------|
|
||||
| **Registry** | `*/definition.py` | `app/modules/discovery.py` |
|
||||
| **Configuration** | `*/config.py` | `app/modules/config.py` |
|
||||
| **API Routes** | `*/routes/api/*.py` | `app/modules/routes.py` |
|
||||
| **Page Routes** | `*/routes/pages/*.py` | `app/modules/routes.py` |
|
||||
| **Tasks** | `*/tasks/__init__.py` | `app/modules/tasks.py` |
|
||||
| **Templates** | `*/templates/` | `app/templates_config.py` |
|
||||
| **Static Files** | `*/static/` | `main.py` |
|
||||
| **Locales** | `*/locales/*.json` | `app/utils/i18n.py` |
|
||||
| **Migrations** | `*/migrations/versions/` | `app/modules/migrations.py` |
|
||||
|
||||
### Creating a New Module (Zero Framework Changes)
|
||||
|
||||
```bash
|
||||
# 1. Create module directory
|
||||
mkdir -p app/modules/mymodule/{routes/{api,pages},services,models,schemas,templates/mymodule/vendor,static/vendor/js,locales,tasks}
|
||||
|
||||
# 2. Create required files
|
||||
touch app/modules/mymodule/__init__.py
|
||||
touch app/modules/mymodule/definition.py
|
||||
touch app/modules/mymodule/exceptions.py
|
||||
|
||||
# 3. That's it! The framework auto-discovers and registers everything.
|
||||
```
|
||||
|
||||
## Three-Tier Classification
|
||||
|
||||
### Core Modules (4)
|
||||
@@ -69,6 +106,160 @@ Internal modules are **admin-only tools** not exposed to customers or vendors.
|
||||
| `dev-tools` | Component library, icon browser |
|
||||
| `monitoring` | Logs, background tasks, Flower, Grafana integration |
|
||||
|
||||
## Self-Contained Module Structure
|
||||
|
||||
Every module follows this standardized structure:
|
||||
|
||||
```
|
||||
app/modules/analytics/
|
||||
├── __init__.py # Module package marker
|
||||
├── definition.py # ModuleDefinition (REQUIRED for auto-discovery)
|
||||
├── config.py # Environment config (auto-discovered)
|
||||
├── exceptions.py # Module-specific exceptions
|
||||
├── routes/
|
||||
│ ├── __init__.py
|
||||
│ ├── api/ # API endpoints (auto-discovered)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── admin.py # Must export: router = APIRouter()
|
||||
│ │ └── vendor.py # Must export: router = APIRouter()
|
||||
│ └── pages/ # HTML page routes (auto-discovered)
|
||||
│ ├── __init__.py
|
||||
│ └── vendor.py # Must export: router = APIRouter()
|
||||
├── services/
|
||||
│ ├── __init__.py
|
||||
│ └── stats_service.py
|
||||
├── models/
|
||||
│ ├── __init__.py
|
||||
│ └── report.py
|
||||
├── schemas/
|
||||
│ ├── __init__.py
|
||||
│ └── stats.py
|
||||
├── templates/ # Auto-discovered by Jinja2
|
||||
│ └── analytics/
|
||||
│ └── vendor/
|
||||
│ └── analytics.html
|
||||
├── static/ # Auto-mounted at /static/modules/analytics/
|
||||
│ └── vendor/
|
||||
│ └── js/
|
||||
│ └── analytics.js
|
||||
├── locales/ # Auto-loaded translations
|
||||
│ ├── en.json
|
||||
│ ├── de.json
|
||||
│ ├── fr.json
|
||||
│ └── lu.json
|
||||
├── tasks/ # Auto-discovered by Celery
|
||||
│ ├── __init__.py # REQUIRED for Celery discovery
|
||||
│ └── reports.py
|
||||
└── migrations/ # Auto-discovered by Alembic
|
||||
├── __init__.py # REQUIRED for discovery
|
||||
└── versions/
|
||||
├── __init__.py # REQUIRED for discovery
|
||||
└── analytics_001_create_reports.py
|
||||
```
|
||||
|
||||
## Module Definition
|
||||
|
||||
Each module must have a `definition.py` with a `ModuleDefinition` instance:
|
||||
|
||||
```python
|
||||
# app/modules/analytics/definition.py
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
analytics_module = ModuleDefinition(
|
||||
# Identity
|
||||
code="analytics",
|
||||
name="Analytics & Reporting",
|
||||
description="Dashboard analytics, custom reports, and data exports.",
|
||||
version="1.0.0",
|
||||
|
||||
# Classification (determines tier)
|
||||
is_core=False, # Set True for core modules
|
||||
is_internal=False, # Set True for admin-only modules
|
||||
|
||||
# Dependencies
|
||||
requires=[], # List other module codes this depends on
|
||||
|
||||
# Features (for tier-based gating)
|
||||
features=[
|
||||
"basic_reports",
|
||||
"analytics_dashboard",
|
||||
"custom_reports",
|
||||
],
|
||||
|
||||
# Menu items per frontend
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [], # Analytics uses dashboard
|
||||
FrontendType.VENDOR: ["analytics"],
|
||||
},
|
||||
|
||||
# Self-contained module configuration
|
||||
is_self_contained=True,
|
||||
services_path="app.modules.analytics.services",
|
||||
models_path="app.modules.analytics.models",
|
||||
schemas_path="app.modules.analytics.schemas",
|
||||
exceptions_path="app.modules.analytics.exceptions",
|
||||
templates_path="templates",
|
||||
locales_path="locales",
|
||||
)
|
||||
```
|
||||
|
||||
### ModuleDefinition Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `code` | `str` | Unique identifier (e.g., "billing") |
|
||||
| `name` | `str` | Display name |
|
||||
| `description` | `str` | What the module provides |
|
||||
| `version` | `str` | Semantic version (default: "1.0.0") |
|
||||
| `requires` | `list[str]` | Module codes this depends on |
|
||||
| `features` | `list[str]` | Feature codes for tier gating |
|
||||
| `menu_items` | `dict` | Menu items per frontend type |
|
||||
| `is_core` | `bool` | Cannot be disabled if True |
|
||||
| `is_internal` | `bool` | Admin-only if True |
|
||||
| `is_self_contained` | `bool` | Uses self-contained structure |
|
||||
|
||||
## Route Auto-Discovery
|
||||
|
||||
Routes in `routes/api/` and `routes/pages/` are automatically discovered and registered.
|
||||
|
||||
### API Routes (`routes/api/vendor.py`)
|
||||
|
||||
```python
|
||||
# app/modules/analytics/routes/api/vendor.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.api.deps import get_current_vendor_api, get_db
|
||||
|
||||
router = APIRouter() # MUST be named 'router' for auto-discovery
|
||||
|
||||
@router.get("")
|
||||
def get_analytics(
|
||||
current_user = Depends(get_current_vendor_api),
|
||||
db = Depends(get_db),
|
||||
):
|
||||
"""Get vendor analytics."""
|
||||
pass
|
||||
```
|
||||
|
||||
**Auto-registered at:** `/api/v1/vendor/analytics`
|
||||
|
||||
### Page Routes (`routes/pages/vendor.py`)
|
||||
|
||||
```python
|
||||
# app/modules/analytics/routes/pages/vendor.py
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
router = APIRouter() # MUST be named 'router' for auto-discovery
|
||||
|
||||
@router.get("/{vendor_code}/analytics", response_class=HTMLResponse)
|
||||
async def analytics_page(request: Request, vendor_code: str):
|
||||
"""Render analytics page."""
|
||||
pass
|
||||
```
|
||||
|
||||
**Auto-registered at:** `/vendor/{vendor_code}/analytics`
|
||||
|
||||
## Framework Layer
|
||||
|
||||
The Framework Layer provides infrastructure that modules depend on. These are **not modules** - they're always available and cannot be disabled.
|
||||
@@ -87,74 +278,6 @@ The Framework Layer provides infrastructure that modules depend on. These are **
|
||||
| Dependencies | `app/api/deps.py` | FastAPI DI |
|
||||
| Base Exceptions | `app/exceptions/base.py` | Exception hierarchy |
|
||||
|
||||
## Module Definition
|
||||
|
||||
Each module is defined using the `ModuleDefinition` dataclass:
|
||||
|
||||
```python
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
billing_module = ModuleDefinition(
|
||||
# Identity
|
||||
code="billing",
|
||||
name="Billing & Subscriptions",
|
||||
description="Platform subscriptions and vendor invoices",
|
||||
version="1.0.0",
|
||||
|
||||
# Dependencies
|
||||
requires=["payments"], # Must have payments enabled
|
||||
|
||||
# Features (for tier-based gating)
|
||||
features=[
|
||||
"subscription_management",
|
||||
"billing_history",
|
||||
"invoice_generation",
|
||||
],
|
||||
|
||||
# Menu items per frontend
|
||||
menu_items={
|
||||
FrontendType.ADMIN: ["subscription-tiers", "subscriptions"],
|
||||
FrontendType.VENDOR: ["billing", "invoices"],
|
||||
},
|
||||
|
||||
# Classification
|
||||
is_core=False,
|
||||
is_internal=False,
|
||||
|
||||
# Configuration schema (optional)
|
||||
config_schema=BillingConfig,
|
||||
default_config={"trial_days": 14},
|
||||
|
||||
# Lifecycle hooks (optional)
|
||||
on_enable=lambda platform_id: setup_billing(platform_id),
|
||||
on_disable=lambda platform_id: cleanup_billing(platform_id),
|
||||
health_check=lambda: {"status": "healthy"},
|
||||
)
|
||||
```
|
||||
|
||||
### ModuleDefinition Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `code` | `str` | Unique identifier (e.g., "billing") |
|
||||
| `name` | `str` | Display name |
|
||||
| `description` | `str` | What the module provides |
|
||||
| `version` | `str` | Semantic version (default: "1.0.0") |
|
||||
| `requires` | `list[str]` | Module codes this depends on |
|
||||
| `features` | `list[str]` | Feature codes for tier gating |
|
||||
| `menu_items` | `dict` | Menu items per frontend type |
|
||||
| `permissions` | `list[str]` | Permission codes defined by module |
|
||||
| `is_core` | `bool` | Cannot be disabled if True |
|
||||
| `is_internal` | `bool` | Admin-only if True |
|
||||
| `config_schema` | `type[BaseModel]` | Pydantic model for configuration |
|
||||
| `default_config` | `dict` | Default configuration values |
|
||||
| `on_enable` | `Callable` | Called when module is enabled |
|
||||
| `on_disable` | `Callable` | Called when module is disabled |
|
||||
| `on_startup` | `Callable` | Called on application startup |
|
||||
| `health_check` | `Callable` | Returns health status dict |
|
||||
| `migrations_path` | `str` | Path to module migrations (relative) |
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
Modules can depend on other modules. When enabling a module, its dependencies are automatically enabled.
|
||||
@@ -178,18 +301,17 @@ Modules can depend on other modules. When enabling a module, its dependencies ar
|
||||
|
||||
## Module Registry
|
||||
|
||||
All modules are registered in `app/modules/registry.py`:
|
||||
The registry auto-discovers all modules:
|
||||
|
||||
```python
|
||||
from app.modules.registry import (
|
||||
MODULES, # All modules
|
||||
MODULES, # All modules (auto-discovered)
|
||||
CORE_MODULES, # Core only
|
||||
OPTIONAL_MODULES, # Optional only
|
||||
INTERNAL_MODULES, # Internal only
|
||||
get_module,
|
||||
get_core_module_codes,
|
||||
get_module_tier,
|
||||
is_core_module,
|
||||
)
|
||||
|
||||
# Get a specific module
|
||||
@@ -211,7 +333,6 @@ from app.modules.service import module_service
|
||||
|
||||
# Check if module is enabled
|
||||
if module_service.is_module_enabled(db, platform_id, "billing"):
|
||||
# Module is enabled for this platform
|
||||
pass
|
||||
|
||||
# Get all enabled modules for a platform
|
||||
@@ -222,178 +343,144 @@ module_service.enable_module(db, platform_id, "billing", user_id=current_user.id
|
||||
|
||||
# Disable a module (auto-disables dependents)
|
||||
module_service.disable_module(db, platform_id, "billing", user_id=current_user.id)
|
||||
|
||||
# Get module configuration
|
||||
config = module_service.get_module_config(db, platform_id, "billing")
|
||||
|
||||
# Set module configuration
|
||||
module_service.set_module_config(db, platform_id, "billing", {"trial_days": 30})
|
||||
```
|
||||
|
||||
## Module Events
|
||||
## Module Configuration
|
||||
|
||||
The event system allows components to react to module lifecycle changes:
|
||||
Modules can have environment-based configuration using Pydantic Settings. The `config.py` file is auto-discovered by `app/modules/config.py`.
|
||||
|
||||
```python
|
||||
from app.modules.events import module_event_bus, ModuleEvent, ModuleEventData
|
||||
# app/modules/marketplace/config.py
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
# Subscribe to events
|
||||
@module_event_bus.subscribe(ModuleEvent.ENABLED)
|
||||
def on_module_enabled(data: ModuleEventData):
|
||||
print(f"Module {data.module_code} enabled for platform {data.platform_id}")
|
||||
class MarketplaceConfig(BaseSettings):
|
||||
"""Configuration for marketplace module."""
|
||||
|
||||
@module_event_bus.subscribe(ModuleEvent.DISABLED)
|
||||
def on_module_disabled(data: ModuleEventData):
|
||||
clear_module_cache(data.platform_id, data.module_code)
|
||||
# Settings loaded from env vars with MARKETPLACE_ prefix
|
||||
api_timeout: int = Field(default=30, description="API timeout in seconds")
|
||||
batch_size: int = Field(default=100, description="Import batch size")
|
||||
max_retries: int = Field(default=3, description="Max retry attempts")
|
||||
|
||||
@module_event_bus.subscribe(ModuleEvent.CONFIG_CHANGED)
|
||||
def on_config_changed(data: ModuleEventData):
|
||||
print(f"Config changed: {data.config}")
|
||||
model_config = {"env_prefix": "MARKETPLACE_"}
|
||||
|
||||
# Events are emitted by ModuleService automatically
|
||||
# You can also emit manually:
|
||||
module_event_bus.emit_enabled("billing", platform_id=1, user_id=42)
|
||||
# Export for auto-discovery
|
||||
config_class = MarketplaceConfig
|
||||
config = MarketplaceConfig()
|
||||
```
|
||||
|
||||
### Event Types
|
||||
**Usage:**
|
||||
|
||||
| Event | When Fired | Data Available |
|
||||
|-------|------------|----------------|
|
||||
| `ENABLED` | Module enabled for platform | `module_code`, `platform_id`, `user_id` |
|
||||
| `DISABLED` | Module disabled for platform | `module_code`, `platform_id`, `user_id` |
|
||||
| `STARTUP` | Application starting | `module_code` |
|
||||
| `SHUTDOWN` | Application shutting down | `module_code` |
|
||||
| `CONFIG_CHANGED` | Module config updated | `module_code`, `platform_id`, `config` |
|
||||
```python
|
||||
# Direct import
|
||||
from app.modules.marketplace.config import config
|
||||
timeout = config.api_timeout
|
||||
|
||||
## Module-Specific Migrations
|
||||
|
||||
Self-contained modules can have their own database migrations:
|
||||
|
||||
```
|
||||
app/modules/cms/
|
||||
├── migrations/
|
||||
│ └── versions/
|
||||
│ ├── cms_001_create_content_pages.py
|
||||
│ └── cms_002_add_seo_fields.py
|
||||
├── models/
|
||||
├── services/
|
||||
└── ...
|
||||
# Via discovery
|
||||
from app.modules.config import get_module_config
|
||||
config = get_module_config("marketplace")
|
||||
```
|
||||
|
||||
**Migration Naming Convention:**
|
||||
**Environment variables:**
|
||||
```bash
|
||||
MARKETPLACE_API_TIMEOUT=60
|
||||
MARKETPLACE_BATCH_SIZE=500
|
||||
```
|
||||
|
||||
## Module Migrations
|
||||
|
||||
Each module owns its database migrations in the `migrations/versions/` directory. Alembic auto-discovers these via `app/modules/migrations.py`.
|
||||
|
||||
### Migration Structure
|
||||
|
||||
```
|
||||
app/modules/cms/migrations/
|
||||
├── __init__.py # REQUIRED for discovery
|
||||
└── versions/
|
||||
├── __init__.py # REQUIRED for discovery
|
||||
├── cms_001_create_content_pages.py
|
||||
├── cms_002_add_sections.py
|
||||
└── cms_003_add_media_library.py
|
||||
```
|
||||
|
||||
### Naming Convention
|
||||
|
||||
```
|
||||
{module_code}_{sequence}_{description}.py
|
||||
```
|
||||
|
||||
Alembic automatically discovers module migrations:
|
||||
### Migration Template
|
||||
|
||||
```python
|
||||
# In alembic/env.py
|
||||
from app.modules.migrations import get_all_migration_paths
|
||||
# app/modules/cms/migrations/versions/cms_001_create_content_pages.py
|
||||
"""Create content_pages table.
|
||||
|
||||
version_locations = [str(p) for p in get_all_migration_paths()]
|
||||
Revision ID: cms_001
|
||||
Create Date: 2026-01-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "cms_001"
|
||||
down_revision = None
|
||||
branch_labels = ("cms",) # Module-specific branch
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"content_pages",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("vendor_id", sa.Integer(), sa.ForeignKey("vendors.id")),
|
||||
sa.Column("slug", sa.String(100), nullable=False),
|
||||
sa.Column("title", sa.String(200), nullable=False),
|
||||
)
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("content_pages")
|
||||
```
|
||||
|
||||
## Self-Contained Module Structure
|
||||
### Running Migrations
|
||||
|
||||
Modules can be self-contained with their own services, models, and templates:
|
||||
Module migrations are automatically discovered:
|
||||
|
||||
```
|
||||
app/modules/cms/
|
||||
├── __init__.py
|
||||
├── definition.py # ModuleDefinition
|
||||
├── config.py # Configuration schema (optional)
|
||||
├── exceptions.py # Module-specific exceptions
|
||||
├── routes/
|
||||
│ ├── admin.py # Admin API routes
|
||||
│ └── vendor.py # Vendor API routes
|
||||
├── services/
|
||||
│ └── content_service.py
|
||||
├── models/
|
||||
│ └── content_page.py
|
||||
├── schemas/
|
||||
│ └── content.py # Pydantic schemas
|
||||
├── templates/
|
||||
│ ├── admin/
|
||||
│ └── vendor/
|
||||
├── migrations/
|
||||
│ └── versions/
|
||||
└── locales/
|
||||
├── en.json
|
||||
└── fr.json
|
||||
```bash
|
||||
# Run all migrations (core + modules)
|
||||
alembic upgrade head
|
||||
|
||||
# View migration history
|
||||
alembic history
|
||||
```
|
||||
|
||||
Configure paths in the definition:
|
||||
### Current State
|
||||
|
||||
```python
|
||||
cms_module = ModuleDefinition(
|
||||
code="cms",
|
||||
name="Content Management",
|
||||
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",
|
||||
migrations_path="migrations",
|
||||
)
|
||||
```
|
||||
Currently, all migrations reside in central `alembic/versions/`. The module-specific directories are in place for:
|
||||
- **New modules**: Should create migrations in their own `migrations/versions/`
|
||||
- **Future reorganization**: Existing migrations will be moved to modules pre-production
|
||||
|
||||
## Database Storage
|
||||
## Architecture Validation Rules
|
||||
|
||||
Module enablement is stored in the `platform_modules` table:
|
||||
The architecture validator (`scripts/validate_architecture.py`) enforces module structure:
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `platform_id` | FK | Platform reference |
|
||||
| `module_code` | string | Module identifier |
|
||||
| `is_enabled` | boolean | Whether enabled |
|
||||
| `enabled_at` | timestamp | When enabled |
|
||||
| `enabled_by_user_id` | FK | Who enabled it |
|
||||
| `disabled_at` | timestamp | When disabled |
|
||||
| `disabled_by_user_id` | FK | Who disabled it |
|
||||
| `config` | JSON | Module-specific configuration |
|
||||
| Rule | Severity | Description |
|
||||
|------|----------|-------------|
|
||||
| MOD-001 | ERROR | Self-contained modules must have required directories |
|
||||
| MOD-002 | WARNING | Services must contain actual code, not re-exports |
|
||||
| MOD-003 | WARNING | Schemas must contain actual code, not re-exports |
|
||||
| MOD-004 | WARNING | Routes must import from module, not legacy locations |
|
||||
| MOD-005 | WARNING | Modules with UI must have templates and static |
|
||||
| MOD-006 | INFO | Modules should have locales for i18n |
|
||||
| MOD-007 | ERROR | Definition paths must match directory structure |
|
||||
| MOD-008 | WARNING | Self-contained modules must have exceptions.py |
|
||||
| MOD-009 | ERROR | Modules must have definition.py for auto-discovery |
|
||||
| MOD-010 | WARNING | Route files must export `router` variable |
|
||||
| MOD-011 | WARNING | Tasks directory must have `__init__.py` |
|
||||
| MOD-012 | INFO | Locales should have all language files |
|
||||
| MOD-013 | INFO | config.py should export `config` or `config_class` |
|
||||
| MOD-014 | WARNING | Migrations must follow naming convention |
|
||||
| MOD-015 | WARNING | Migrations directory must have `__init__.py` files |
|
||||
|
||||
## Menu Item Filtering
|
||||
|
||||
Menu items are filtered based on enabled modules:
|
||||
|
||||
```python
|
||||
from app.modules.service import module_service
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
# Get available menu items for platform
|
||||
menu_items = module_service.get_module_menu_items(
|
||||
db, platform_id, FrontendType.ADMIN
|
||||
)
|
||||
|
||||
# Check if specific menu item's module is enabled
|
||||
is_available = module_service.is_menu_item_module_enabled(
|
||||
db, platform_id, "subscription-tiers", FrontendType.ADMIN
|
||||
)
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
Modules can provide health checks that are aggregated:
|
||||
|
||||
```python
|
||||
from app.core.observability import health_registry, register_module_health_checks
|
||||
|
||||
# Register all module health checks on startup
|
||||
register_module_health_checks()
|
||||
|
||||
# Health endpoint aggregates results
|
||||
# GET /health returns:
|
||||
{
|
||||
"status": "healthy",
|
||||
"checks": [
|
||||
{"name": "module:billing", "status": "healthy"},
|
||||
{"name": "module:payments", "status": "healthy"},
|
||||
...
|
||||
]
|
||||
}
|
||||
Run validation:
|
||||
```bash
|
||||
python scripts/validate_architecture.py
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
@@ -404,7 +491,9 @@ register_module_health_checks()
|
||||
- Use `requires` for hard dependencies
|
||||
- Provide `health_check` for critical modules
|
||||
- Use events for cross-module communication
|
||||
- Document module features and menu items
|
||||
- Follow the standard directory structure
|
||||
- Export `router` variable in route files
|
||||
- Include all supported languages in locales
|
||||
|
||||
### Don't
|
||||
|
||||
@@ -412,12 +501,12 @@ register_module_health_checks()
|
||||
- Make core modules depend on optional modules
|
||||
- Put framework-level code in modules
|
||||
- Skip migration naming conventions
|
||||
- Forget to register menu items
|
||||
- Forget `__init__.py` in tasks directory
|
||||
- Manually register modules in registry.py (use auto-discovery)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Menu Management](menu-management.md) - Sidebar and menu configuration
|
||||
- [Creating Modules](../development/creating-modules.md) - Developer guide for building modules
|
||||
- [Observability](observability.md) - Health checks and module health integration
|
||||
- [Multi-Tenant System](multi-tenant.md) - Platform isolation
|
||||
- [Creating Modules](../development/creating-modules.md) - Step-by-step guide
|
||||
- [Menu Management](menu-management.md) - Sidebar configuration
|
||||
- [Observability](observability.md) - Health checks integration
|
||||
- [Feature Gating](../implementation/feature-gating-system.md) - Tier-based access
|
||||
|
||||
@@ -1,305 +1,437 @@
|
||||
# Creating Modules
|
||||
|
||||
This guide explains how to create new modules for the Wizamart platform, including both simple wrapper modules and self-contained modules with their own services, models, and migrations.
|
||||
This guide explains how to create new **plug-and-play modules** for the Wizamart platform. Modules are fully self-contained and automatically discovered - no changes to framework files required.
|
||||
|
||||
## Module Types
|
||||
## Quick Start (5 Minutes)
|
||||
|
||||
| Type | Complexity | Use Case |
|
||||
|------|------------|----------|
|
||||
| **Wrapper Module** | Simple | Groups existing routes and features under a toggleable module |
|
||||
| **Self-Contained Module** | Complex | Full isolation with own services, models, templates, migrations |
|
||||
Creating a new module requires **zero changes** to `main.py`, `registry.py`, or any other framework file.
|
||||
|
||||
## Quick Start: Wrapper Module
|
||||
### Step 1: Create Directory Structure
|
||||
|
||||
For a simple module that wraps existing functionality:
|
||||
```bash
|
||||
# Create module with all directories
|
||||
MODULE_NAME=mymodule
|
||||
|
||||
mkdir -p app/modules/$MODULE_NAME/{routes/{api,pages},services,models,schemas,templates/$MODULE_NAME/vendor,static/vendor/js,locales,tasks}
|
||||
|
||||
# Create required files
|
||||
touch app/modules/$MODULE_NAME/__init__.py
|
||||
touch app/modules/$MODULE_NAME/definition.py
|
||||
touch app/modules/$MODULE_NAME/exceptions.py
|
||||
touch app/modules/$MODULE_NAME/routes/__init__.py
|
||||
touch app/modules/$MODULE_NAME/routes/api/__init__.py
|
||||
touch app/modules/$MODULE_NAME/routes/pages/__init__.py
|
||||
touch app/modules/$MODULE_NAME/services/__init__.py
|
||||
touch app/modules/$MODULE_NAME/models/__init__.py
|
||||
touch app/modules/$MODULE_NAME/schemas/__init__.py
|
||||
touch app/modules/$MODULE_NAME/tasks/__init__.py
|
||||
```
|
||||
|
||||
### Step 2: Create Module Definition
|
||||
|
||||
```python
|
||||
# app/modules/analytics/definition.py
|
||||
# app/modules/mymodule/definition.py
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
analytics_module = ModuleDefinition(
|
||||
code="analytics",
|
||||
name="Analytics & Reports",
|
||||
description="Business analytics, reports, and dashboards",
|
||||
mymodule_module = ModuleDefinition(
|
||||
code="mymodule",
|
||||
name="My Module",
|
||||
description="Description of what this module does",
|
||||
version="1.0.0",
|
||||
features=[
|
||||
"analytics_dashboard",
|
||||
"sales_reports",
|
||||
"customer_insights",
|
||||
],
|
||||
|
||||
# Classification
|
||||
is_core=False, # True = always enabled
|
||||
is_internal=False, # True = admin-only
|
||||
is_self_contained=True,
|
||||
|
||||
# Features for tier-based gating
|
||||
features=["mymodule_feature"],
|
||||
|
||||
# Menu items
|
||||
menu_items={
|
||||
FrontendType.ADMIN: ["analytics-dashboard", "reports"],
|
||||
FrontendType.VENDOR: ["analytics", "sales-reports"],
|
||||
FrontendType.VENDOR: ["mymodule"],
|
||||
},
|
||||
is_core=False,
|
||||
is_internal=False,
|
||||
|
||||
# Paths (for self-contained modules)
|
||||
services_path="app.modules.mymodule.services",
|
||||
models_path="app.modules.mymodule.models",
|
||||
schemas_path="app.modules.mymodule.schemas",
|
||||
exceptions_path="app.modules.mymodule.exceptions",
|
||||
templates_path="templates",
|
||||
locales_path="locales",
|
||||
)
|
||||
```
|
||||
|
||||
## Module Definition Fields
|
||||
### Step 3: Create Routes (Auto-Discovered)
|
||||
|
||||
### Required Fields
|
||||
```python
|
||||
# app/modules/mymodule/routes/api/vendor.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.api.deps import get_current_vendor_api, get_db
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `code` | str | Unique identifier (e.g., "billing", "analytics") |
|
||||
| `name` | str | Display name (e.g., "Billing & Subscriptions") |
|
||||
router = APIRouter() # MUST be named 'router'
|
||||
|
||||
### Optional Fields
|
||||
@router.get("")
|
||||
def get_mymodule_data(current_user=Depends(get_current_vendor_api)):
|
||||
return {"message": "Hello from mymodule"}
|
||||
```
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `description` | str | "" | What the module provides |
|
||||
| `version` | str | "1.0.0" | Semantic version |
|
||||
| `requires` | list[str] | [] | Module codes this depends on |
|
||||
| `features` | list[str] | [] | Feature codes for tier gating |
|
||||
| `menu_items` | dict | {} | Menu items per frontend type |
|
||||
| `permissions` | list[str] | [] | Permission codes defined |
|
||||
| `is_core` | bool | False | Cannot be disabled if True |
|
||||
| `is_internal` | bool | False | Admin-only if True |
|
||||
```python
|
||||
# app/modules/mymodule/routes/pages/vendor.py
|
||||
from fastapi import APIRouter, Request, Path
|
||||
from fastapi.responses import HTMLResponse
|
||||
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db, Depends
|
||||
from app.templates_config import templates
|
||||
|
||||
### Configuration Fields
|
||||
router = APIRouter() # MUST be named 'router'
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `config_schema` | type[BaseModel] | Pydantic model for validation |
|
||||
| `default_config` | dict | Default configuration values |
|
||||
@router.get("/{vendor_code}/mymodule", response_class=HTMLResponse)
|
||||
async def mymodule_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(...),
|
||||
current_user=Depends(get_current_vendor_from_cookie_or_header),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
return templates.TemplateResponse(
|
||||
"mymodule/vendor/index.html",
|
||||
{"request": request, "vendor_code": vendor_code},
|
||||
)
|
||||
```
|
||||
|
||||
### Lifecycle Hooks
|
||||
### Step 4: Done!
|
||||
|
||||
| Field | Signature | Description |
|
||||
|-------|-----------|-------------|
|
||||
| `on_enable` | (platform_id: int) -> None | Called when enabled |
|
||||
| `on_disable` | (platform_id: int) -> None | Called when disabled |
|
||||
| `on_startup` | () -> None | Called on app startup |
|
||||
| `health_check` | () -> dict | Returns health status |
|
||||
That's it! The framework automatically:
|
||||
- Discovers and registers the module
|
||||
- Mounts API routes at `/api/v1/vendor/mymodule`
|
||||
- Mounts page routes at `/vendor/{code}/mymodule`
|
||||
- Loads templates from `templates/`
|
||||
- Mounts static files at `/static/modules/mymodule/`
|
||||
- Loads translations from `locales/`
|
||||
|
||||
## Complete Module Structure
|
||||
|
||||
```
|
||||
app/modules/mymodule/
|
||||
├── __init__.py # Package marker
|
||||
├── definition.py # ModuleDefinition (REQUIRED)
|
||||
├── config.py # Environment config (optional, auto-discovered)
|
||||
├── exceptions.py # Module exceptions
|
||||
│
|
||||
├── routes/ # Auto-discovered routes
|
||||
│ ├── __init__.py
|
||||
│ ├── api/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── admin.py # /api/v1/admin/mymodule
|
||||
│ │ └── vendor.py # /api/v1/vendor/mymodule
|
||||
│ └── pages/
|
||||
│ ├── __init__.py
|
||||
│ ├── admin.py # /admin/mymodule
|
||||
│ └── vendor.py # /vendor/{code}/mymodule
|
||||
│
|
||||
├── services/ # Business logic
|
||||
│ ├── __init__.py
|
||||
│ └── mymodule_service.py
|
||||
│
|
||||
├── models/ # SQLAlchemy models
|
||||
│ ├── __init__.py
|
||||
│ └── mymodel.py
|
||||
│
|
||||
├── schemas/ # Pydantic schemas
|
||||
│ ├── __init__.py
|
||||
│ └── myschema.py
|
||||
│
|
||||
├── templates/ # Jinja2 templates (auto-loaded)
|
||||
│ └── mymodule/
|
||||
│ ├── admin/
|
||||
│ │ └── index.html
|
||||
│ └── vendor/
|
||||
│ └── index.html
|
||||
│
|
||||
├── static/ # Static files (auto-mounted)
|
||||
│ ├── admin/
|
||||
│ │ └── js/
|
||||
│ │ └── mymodule.js
|
||||
│ └── vendor/
|
||||
│ └── js/
|
||||
│ └── mymodule.js
|
||||
│
|
||||
├── locales/ # Translations (auto-loaded)
|
||||
│ ├── en.json
|
||||
│ ├── de.json
|
||||
│ ├── fr.json
|
||||
│ └── lu.json
|
||||
│
|
||||
├── tasks/ # Celery tasks (auto-discovered)
|
||||
│ ├── __init__.py # REQUIRED for discovery
|
||||
│ └── background_tasks.py
|
||||
│
|
||||
└── migrations/ # Alembic migrations (auto-discovered)
|
||||
├── __init__.py # REQUIRED for discovery
|
||||
└── versions/
|
||||
├── __init__.py # REQUIRED for discovery
|
||||
└── mymodule_001_initial.py
|
||||
```
|
||||
|
||||
## Auto-Discovery Details
|
||||
|
||||
### What Gets Auto-Discovered
|
||||
|
||||
| Component | Location | Discovered By | Notes |
|
||||
|-----------|----------|---------------|-------|
|
||||
| Module Definition | `definition.py` | `app/modules/discovery.py` | Must export ModuleDefinition |
|
||||
| Configuration | `config.py` | `app/modules/config.py` | Must export `config` or `config_class` |
|
||||
| API Routes | `routes/api/*.py` | `app/modules/routes.py` | Must export `router` |
|
||||
| Page Routes | `routes/pages/*.py` | `app/modules/routes.py` | Must export `router` |
|
||||
| Templates | `templates/` | `app/templates_config.py` | Use namespace prefix |
|
||||
| Static Files | `static/` | `main.py` | Mounted at `/static/modules/{code}/` |
|
||||
| Locales | `locales/*.json` | `app/utils/i18n.py` | Keyed by module code |
|
||||
| Tasks | `tasks/` | `app/modules/tasks.py` | Needs `__init__.py` |
|
||||
| Migrations | `migrations/versions/` | `app/modules/migrations.py` | Use naming convention |
|
||||
|
||||
### Route File Requirements
|
||||
|
||||
Route files MUST export a `router` variable:
|
||||
|
||||
```python
|
||||
# CORRECT - will be discovered
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/")
|
||||
def my_endpoint():
|
||||
pass
|
||||
|
||||
# WRONG - won't be discovered
|
||||
my_router = APIRouter() # Variable not named 'router'
|
||||
```
|
||||
|
||||
### Template Namespacing
|
||||
|
||||
Templates must be namespaced under the module code:
|
||||
|
||||
```
|
||||
templates/
|
||||
└── mymodule/ # Module code as namespace
|
||||
└── vendor/
|
||||
└── index.html
|
||||
```
|
||||
|
||||
Reference in code:
|
||||
```python
|
||||
templates.TemplateResponse("mymodule/vendor/index.html", {...})
|
||||
```
|
||||
|
||||
### Static File URLs
|
||||
|
||||
Static files are mounted at `/static/modules/{module_code}/`:
|
||||
|
||||
```html
|
||||
<!-- In template -->
|
||||
<script src="{{ url_for('mymodule_static', path='vendor/js/mymodule.js') }}"></script>
|
||||
```
|
||||
|
||||
### Translation Keys
|
||||
|
||||
Translations are namespaced under the module code:
|
||||
|
||||
```json
|
||||
// locales/en.json
|
||||
{
|
||||
"mymodule": {
|
||||
"title": "My Module",
|
||||
"description": "Module description"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use in templates:
|
||||
```html
|
||||
{{ _('mymodule.title') }}
|
||||
```
|
||||
|
||||
## Module Classification
|
||||
|
||||
### Core Modules
|
||||
|
||||
Cannot be disabled. Use for essential platform functionality.
|
||||
Always enabled, cannot be disabled.
|
||||
|
||||
```python
|
||||
core_module = ModuleDefinition(
|
||||
code="tenancy",
|
||||
name="Platform Tenancy",
|
||||
is_core=True, # Cannot be disabled
|
||||
ModuleDefinition(
|
||||
code="mymodule",
|
||||
is_core=True,
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
### Optional Modules
|
||||
|
||||
### Optional Modules (Default)
|
||||
Can be enabled/disabled per platform.
|
||||
|
||||
```python
|
||||
billing_module = ModuleDefinition(
|
||||
code="billing",
|
||||
name="Billing",
|
||||
is_core=False, # Can be toggled
|
||||
ModuleDefinition(
|
||||
code="mymodule",
|
||||
is_core=False,
|
||||
is_internal=False,
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
### Internal Modules
|
||||
|
||||
Admin-only tools not visible to customers/vendors.
|
||||
Admin-only tools, not visible to vendors.
|
||||
|
||||
```python
|
||||
devtools_module = ModuleDefinition(
|
||||
code="dev-tools",
|
||||
name="Developer Tools",
|
||||
is_internal=True, # Admin-only
|
||||
ModuleDefinition(
|
||||
code="mymodule",
|
||||
is_internal=True,
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
Declare dependencies using the `requires` field:
|
||||
Declare dependencies in the definition:
|
||||
|
||||
```python
|
||||
# orders module requires payments
|
||||
orders_module = ModuleDefinition(
|
||||
ModuleDefinition(
|
||||
code="orders",
|
||||
name="Orders",
|
||||
requires=["payments"], # Must have payments enabled
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
**Dependency Rules:**
|
||||
1. Core modules cannot depend on optional modules
|
||||
2. Enabling a module auto-enables its dependencies
|
||||
3. Disabling a module auto-disables modules that depend on it
|
||||
4. Circular dependencies are not allowed
|
||||
**Rules:**
|
||||
- Enabling a module auto-enables its dependencies
|
||||
- Disabling a module auto-disables modules that depend on it
|
||||
- Core modules cannot depend on optional modules
|
||||
|
||||
## Self-Contained Module Structure
|
||||
|
||||
For modules with their own services, models, and templates:
|
||||
|
||||
```
|
||||
app/modules/cms/
|
||||
├── __init__.py
|
||||
├── definition.py # ModuleDefinition
|
||||
├── config.py # Configuration schema (optional)
|
||||
├── exceptions.py # Module-specific exceptions
|
||||
├── routes/
|
||||
│ ├── __init__.py
|
||||
│ ├── admin.py # Admin API routes
|
||||
│ └── vendor.py # Vendor API routes
|
||||
├── services/
|
||||
│ ├── __init__.py
|
||||
│ └── content_service.py
|
||||
├── models/
|
||||
│ ├── __init__.py
|
||||
│ └── content_page.py
|
||||
├── schemas/
|
||||
│ ├── __init__.py
|
||||
│ └── content.py
|
||||
├── templates/
|
||||
│ ├── admin/
|
||||
│ └── vendor/
|
||||
├── migrations/
|
||||
│ └── versions/
|
||||
│ ├── cms_001_create_content_pages.py
|
||||
│ └── cms_002_add_seo_fields.py
|
||||
└── locales/
|
||||
├── en.json
|
||||
└── fr.json
|
||||
```
|
||||
|
||||
### Self-Contained Definition
|
||||
## Services Pattern
|
||||
|
||||
```python
|
||||
# app/modules/cms/definition.py
|
||||
from pydantic import BaseModel, Field
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
class CMSConfig(BaseModel):
|
||||
"""CMS module configuration."""
|
||||
max_pages_per_vendor: int = Field(default=100, ge=1, le=1000)
|
||||
enable_seo: bool = True
|
||||
default_language: str = "en"
|
||||
|
||||
cms_module = ModuleDefinition(
|
||||
# Identity
|
||||
code="cms",
|
||||
name="Content Management",
|
||||
description="Content pages, media library, and themes",
|
||||
version="1.0.0",
|
||||
|
||||
# Classification
|
||||
is_core=True,
|
||||
is_self_contained=True,
|
||||
|
||||
# Features
|
||||
features=[
|
||||
"content_pages",
|
||||
"media_library",
|
||||
"vendor_themes",
|
||||
],
|
||||
|
||||
# Menu items
|
||||
menu_items={
|
||||
FrontendType.ADMIN: ["content-pages", "vendor-themes"],
|
||||
FrontendType.VENDOR: ["content-pages", "media"],
|
||||
},
|
||||
|
||||
# Configuration
|
||||
config_schema=CMSConfig,
|
||||
default_config={
|
||||
"max_pages_per_vendor": 100,
|
||||
"enable_seo": True,
|
||||
},
|
||||
|
||||
# Self-contained paths
|
||||
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",
|
||||
migrations_path="migrations",
|
||||
|
||||
# Lifecycle
|
||||
health_check=lambda: {"status": "healthy"},
|
||||
)
|
||||
```
|
||||
|
||||
## Creating Module Routes
|
||||
|
||||
### Admin Routes
|
||||
|
||||
```python
|
||||
# app/modules/payments/routes/admin.py
|
||||
from fastapi import APIRouter, Depends
|
||||
# app/modules/mymodule/services/mymodule_service.py
|
||||
from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_db, get_current_admin_user
|
||||
|
||||
admin_router = APIRouter(prefix="/api/admin/payments", tags=["Admin - Payments"])
|
||||
class MyModuleService:
|
||||
def get_data(self, db: Session, vendor_id: int):
|
||||
# Business logic here
|
||||
pass
|
||||
|
||||
@admin_router.get("/gateways")
|
||||
async def list_gateways(
|
||||
db: Session = Depends(get_db),
|
||||
current_user = Depends(get_current_admin_user),
|
||||
):
|
||||
"""List configured payment gateways."""
|
||||
# Implementation
|
||||
pass
|
||||
# Singleton instance
|
||||
mymodule_service = MyModuleService()
|
||||
```
|
||||
|
||||
### Vendor Routes
|
||||
|
||||
```python
|
||||
# app/modules/payments/routes/vendor.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.api.deps import get_db, get_current_vendor_user
|
||||
|
||||
vendor_router = APIRouter(prefix="/api/vendor/payments", tags=["Vendor - Payments"])
|
||||
|
||||
@vendor_router.get("/methods")
|
||||
async def list_payment_methods(
|
||||
db: Session = Depends(get_db),
|
||||
current_user = Depends(get_current_vendor_user),
|
||||
):
|
||||
"""List vendor's stored payment methods."""
|
||||
pass
|
||||
```
|
||||
|
||||
### Lazy Router Loading
|
||||
|
||||
To avoid circular imports, use lazy loading:
|
||||
|
||||
```python
|
||||
# app/modules/payments/definition.py
|
||||
|
||||
def _get_admin_router():
|
||||
"""Lazy import to avoid circular imports."""
|
||||
from app.modules.payments.routes.admin import admin_router
|
||||
return admin_router
|
||||
|
||||
def _get_vendor_router():
|
||||
from app.modules.payments.routes.vendor import vendor_router
|
||||
return vendor_router
|
||||
|
||||
payments_module = ModuleDefinition(
|
||||
code="payments",
|
||||
# ...
|
||||
# app/modules/mymodule/services/__init__.py
|
||||
from app.modules.mymodule.services.mymodule_service import (
|
||||
mymodule_service,
|
||||
MyModuleService,
|
||||
)
|
||||
|
||||
def get_payments_module_with_routers():
|
||||
"""Get module with routers attached."""
|
||||
payments_module.admin_router = _get_admin_router()
|
||||
payments_module.vendor_router = _get_vendor_router()
|
||||
return payments_module
|
||||
__all__ = ["mymodule_service", "MyModuleService"]
|
||||
```
|
||||
|
||||
## Module Migrations
|
||||
## Exceptions Pattern
|
||||
|
||||
```python
|
||||
# app/modules/mymodule/exceptions.py
|
||||
from app.exceptions import WizamartException
|
||||
|
||||
class MyModuleException(WizamartException):
|
||||
"""Base exception for mymodule."""
|
||||
pass
|
||||
|
||||
class MyModuleNotFoundError(MyModuleException):
|
||||
"""Resource not found."""
|
||||
def __init__(self, resource_id: int):
|
||||
super().__init__(f"Resource {resource_id} not found")
|
||||
```
|
||||
|
||||
## Configuration Pattern
|
||||
|
||||
Modules can have their own environment-based configuration using Pydantic Settings.
|
||||
|
||||
```python
|
||||
# app/modules/mymodule/config.py
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class MyModuleConfig(BaseSettings):
|
||||
"""Configuration for mymodule."""
|
||||
|
||||
# Settings are loaded from environment with MYMODULE_ prefix
|
||||
api_timeout: int = Field(default=30, description="API timeout in seconds")
|
||||
max_retries: int = Field(default=3, description="Maximum retry attempts")
|
||||
batch_size: int = Field(default=100, description="Batch processing size")
|
||||
enable_feature_x: bool = Field(default=False, description="Enable feature X")
|
||||
|
||||
model_config = {"env_prefix": "MYMODULE_"}
|
||||
|
||||
|
||||
# Export for auto-discovery
|
||||
config_class = MyModuleConfig
|
||||
config = MyModuleConfig()
|
||||
```
|
||||
|
||||
Access configuration in your code:
|
||||
|
||||
```python
|
||||
# In services or routes
|
||||
from app.modules.config import get_module_config
|
||||
|
||||
mymodule_config = get_module_config("mymodule")
|
||||
timeout = mymodule_config.api_timeout
|
||||
```
|
||||
|
||||
Or import directly:
|
||||
|
||||
```python
|
||||
from app.modules.mymodule.config import config
|
||||
|
||||
timeout = config.api_timeout
|
||||
```
|
||||
|
||||
Environment variables:
|
||||
```bash
|
||||
# .env
|
||||
MYMODULE_API_TIMEOUT=60
|
||||
MYMODULE_MAX_RETRIES=5
|
||||
MYMODULE_ENABLE_FEATURE_X=true
|
||||
```
|
||||
|
||||
## Background Tasks
|
||||
|
||||
```python
|
||||
# app/modules/mymodule/tasks/__init__.py
|
||||
from app.modules.mymodule.tasks.background import process_data
|
||||
|
||||
__all__ = ["process_data"]
|
||||
```
|
||||
|
||||
```python
|
||||
# app/modules/mymodule/tasks/background.py
|
||||
from app.modules.task_base import DatabaseTask
|
||||
from celery_config import celery_app
|
||||
|
||||
@celery_app.task(base=DatabaseTask, bind=True)
|
||||
def process_data(self, data_id: int):
|
||||
"""Process data in background."""
|
||||
db = self.get_db()
|
||||
# Use db session
|
||||
pass
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
Modules can have their own migrations that are auto-discovered by Alembic.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
app/modules/mymodule/migrations/
|
||||
├── __init__.py # REQUIRED for discovery
|
||||
└── versions/
|
||||
├── __init__.py # REQUIRED for discovery
|
||||
├── mymodule_001_initial.py
|
||||
└── mymodule_002_add_feature.py
|
||||
```
|
||||
|
||||
### Naming Convention
|
||||
|
||||
@@ -307,177 +439,121 @@ def get_payments_module_with_routers():
|
||||
{module_code}_{sequence}_{description}.py
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `cms_001_create_content_pages.py`
|
||||
- `cms_002_add_seo_fields.py`
|
||||
- `billing_001_create_subscriptions.py`
|
||||
Example: `mymodule_001_create_tables.py`
|
||||
|
||||
### Creating the Migrations Directory
|
||||
|
||||
```bash
|
||||
# Create migrations directory structure
|
||||
mkdir -p app/modules/mymodule/migrations/versions
|
||||
|
||||
# Create required __init__.py files
|
||||
echo '"""Module migrations."""' > app/modules/mymodule/migrations/__init__.py
|
||||
echo '"""Migration versions."""' > app/modules/mymodule/migrations/versions/__init__.py
|
||||
```
|
||||
|
||||
### Migration Template
|
||||
|
||||
```python
|
||||
# app/modules/cms/migrations/versions/cms_001_create_content_pages.py
|
||||
"""Create content pages table.
|
||||
|
||||
Revision ID: cms_001
|
||||
Revises:
|
||||
Create Date: 2026-01-27
|
||||
# app/modules/mymodule/migrations/versions/mymodule_001_create_tables.py
|
||||
"""Create mymodule tables.
|
||||
|
||||
Revision ID: mymodule_001
|
||||
Create Date: 2026-01-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "cms_001"
|
||||
down_revision = None # Or previous module migration
|
||||
branch_labels = ("cms",)
|
||||
depends_on = None
|
||||
revision = "mymodule_001"
|
||||
down_revision = None
|
||||
branch_labels = ("mymodule",)
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"cms_content_pages",
|
||||
"mymodule_items",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("vendor_id", sa.Integer(), sa.ForeignKey("vendors.id")),
|
||||
sa.Column("title", sa.String(200), nullable=False),
|
||||
sa.Column("slug", sa.String(200), nullable=False),
|
||||
sa.Column("content", sa.Text()),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True)),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True)),
|
||||
sa.Column("name", sa.String(200), nullable=False),
|
||||
)
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("cms_content_pages")
|
||||
op.drop_table("mymodule_items")
|
||||
```
|
||||
|
||||
## Module Configuration
|
||||
### Running Module Migrations
|
||||
|
||||
### Defining Configuration Schema
|
||||
Module migrations are automatically discovered. Run all migrations:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class BillingConfig(BaseModel):
|
||||
"""Billing module configuration."""
|
||||
|
||||
stripe_mode: Literal["test", "live"] = "test"
|
||||
trial_days: int = Field(default=14, ge=0, le=365)
|
||||
enable_invoices: bool = True
|
||||
invoice_prefix: str = Field(default="INV-", max_length=10)
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
### Using Configuration
|
||||
The `alembic/env.py` automatically discovers module migrations via `app/modules/migrations.py`.
|
||||
|
||||
```python
|
||||
from app.modules.service import module_service
|
||||
### Note on Existing Modules
|
||||
|
||||
# Get module config for platform
|
||||
config = module_service.get_module_config(db, platform_id, "billing")
|
||||
# Returns: {"stripe_mode": "test", "trial_days": 14, ...}
|
||||
Existing modules (CMS, billing, marketplace, etc.) currently have their migrations in the central `alembic/versions/` directory. Moving these to module-specific directories is an optional migration task that can be done incrementally.
|
||||
|
||||
# Set module config
|
||||
module_service.set_module_config(
|
||||
db, platform_id, "billing",
|
||||
{"trial_days": 30}
|
||||
)
|
||||
**New modules should create their own `migrations/versions/` directory.**
|
||||
|
||||
## Validation
|
||||
|
||||
Run the architecture validator to check your module:
|
||||
|
||||
```bash
|
||||
python scripts/validate_architecture.py
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
### Validation Rules
|
||||
|
||||
### Defining Health Check
|
||||
|
||||
```python
|
||||
def check_billing_health() -> dict:
|
||||
"""Check billing service dependencies."""
|
||||
issues = []
|
||||
|
||||
# Check Stripe
|
||||
try:
|
||||
stripe.Account.retrieve()
|
||||
except stripe.AuthenticationError:
|
||||
issues.append("Stripe authentication failed")
|
||||
|
||||
if issues:
|
||||
return {
|
||||
"status": "unhealthy",
|
||||
"message": "; ".join(issues),
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"details": {"stripe": "connected"},
|
||||
}
|
||||
|
||||
billing_module = ModuleDefinition(
|
||||
code="billing",
|
||||
health_check=check_billing_health,
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
## Registering the Module
|
||||
|
||||
### Add to Registry
|
||||
|
||||
```python
|
||||
# app/modules/registry.py
|
||||
|
||||
from app.modules.analytics.definition import analytics_module
|
||||
|
||||
OPTIONAL_MODULES: dict[str, ModuleDefinition] = {
|
||||
# ... existing modules
|
||||
"analytics": analytics_module,
|
||||
}
|
||||
|
||||
MODULES = {**CORE_MODULES, **OPTIONAL_MODULES, **INTERNAL_MODULES}
|
||||
```
|
||||
|
||||
## Module Events
|
||||
|
||||
Subscribe to module lifecycle events:
|
||||
|
||||
```python
|
||||
from app.modules.events import module_event_bus, ModuleEvent, ModuleEventData
|
||||
|
||||
@module_event_bus.subscribe(ModuleEvent.ENABLED)
|
||||
def on_analytics_enabled(data: ModuleEventData):
|
||||
if data.module_code == "analytics":
|
||||
# Initialize analytics for this platform
|
||||
setup_analytics_dashboard(data.platform_id)
|
||||
|
||||
@module_event_bus.subscribe(ModuleEvent.DISABLED)
|
||||
def on_analytics_disabled(data: ModuleEventData):
|
||||
if data.module_code == "analytics":
|
||||
# Cleanup
|
||||
cleanup_analytics_data(data.platform_id)
|
||||
```
|
||||
| Rule | What It Checks |
|
||||
|------|----------------|
|
||||
| MOD-001 | Required directories exist |
|
||||
| MOD-002 | Services contain actual code |
|
||||
| MOD-003 | Schemas contain actual code |
|
||||
| MOD-004 | Routes import from module |
|
||||
| MOD-005 | Templates and static exist |
|
||||
| MOD-006 | Locales directory exists |
|
||||
| MOD-007 | Definition paths are valid |
|
||||
| MOD-008 | exceptions.py exists |
|
||||
| MOD-009 | definition.py exists |
|
||||
| MOD-010 | Routes export `router` |
|
||||
| MOD-011 | Tasks has `__init__.py` |
|
||||
| MOD-012 | All locale files exist |
|
||||
| MOD-013 | config.py exports config |
|
||||
| MOD-014 | Migrations follow naming convention |
|
||||
| MOD-015 | Migrations have `__init__.py` |
|
||||
|
||||
## Checklist
|
||||
|
||||
### New Module Checklist
|
||||
|
||||
- [ ] Create module directory: `app/modules/{code}/`
|
||||
- [ ] Create module directory with structure
|
||||
- [ ] Create `definition.py` with ModuleDefinition
|
||||
- [ ] Add module to appropriate dict in `registry.py`
|
||||
- [ ] Create routes if needed (admin.py, vendor.py)
|
||||
- [ ] Register menu items in menu registry
|
||||
- [ ] Create migrations if adding database tables
|
||||
- [ ] Add health check if module has dependencies
|
||||
- [ ] Document features and configuration options
|
||||
- [ ] Write tests for module functionality
|
||||
- [ ] Set `is_self_contained=True`
|
||||
- [ ] Create `__init__.py` in all directories
|
||||
- [ ] Create `exceptions.py`
|
||||
- [ ] Create routes with `router` variable
|
||||
- [ ] Create templates with namespace prefix
|
||||
- [ ] Create locales (en, de, fr, lu)
|
||||
- [ ] Create `config.py` if module needs environment settings (optional)
|
||||
- [ ] Create `migrations/versions/` with `__init__.py` files if module has database tables
|
||||
- [ ] Run `python scripts/validate_architecture.py`
|
||||
- [ ] Test routes are accessible
|
||||
|
||||
### Self-Contained Module Checklist
|
||||
### No Framework Changes Needed
|
||||
|
||||
- [ ] All items from basic checklist
|
||||
- [ ] Create `services/` directory with business logic
|
||||
- [ ] Create `models/` directory with SQLAlchemy models
|
||||
- [ ] Create `schemas/` directory with Pydantic schemas
|
||||
- [ ] Create `exceptions.py` for module-specific errors
|
||||
- [ ] Create `config.py` with Pydantic config model
|
||||
- [ ] Set up `migrations/versions/` directory
|
||||
- [ ] Create `templates/` if module has UI
|
||||
- [ ] Create `locales/` if module needs translations
|
||||
- [ ] Set `is_self_contained=True` and path attributes
|
||||
You do NOT need to:
|
||||
- Edit `main.py`
|
||||
- Edit `registry.py`
|
||||
- Edit any configuration files
|
||||
- Register routes manually
|
||||
- Import the module anywhere
|
||||
|
||||
The framework discovers everything automatically!
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Module System](../architecture/module-system.md) - Architecture overview
|
||||
- [Menu Management](../architecture/menu-management.md) - Menu item integration
|
||||
- [Database Migrations](migration/database-migrations.md) - Migration guide
|
||||
- [Menu Management](../architecture/menu-management.md) - Sidebar integration
|
||||
- [Architecture Rules](architecture-rules.md) - Validation rules
|
||||
|
||||
Reference in New Issue
Block a user