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