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:
2026-01-28 22:19:41 +01:00
parent eb47daec8b
commit 2466dfd7ed
43 changed files with 2338 additions and 581 deletions

View 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"