Files
orion/docs/architecture/cross-module-import-rules.md
Samir Boulahtit 3e38db79aa feat: implement DashboardWidgetProvider pattern for modular dashboard widgets
Add protocol-based widget system following the MetricsProvider pattern:

- Create DashboardWidgetProviderProtocol in contracts/widgets.py
- Add WidgetAggregatorService in core to discover and aggregate widgets
- Implement MarketplaceWidgetProvider for recent_imports widget
- Implement TenancyWidgetProvider for recent_vendors widget
- Update admin dashboard to use widget_aggregator
- Add widget_provider field to ModuleDefinition

Architecture documentation:
- Add widget-provider-pattern.md with implementation guide
- Add cross-module-import-rules.md enforcing core/optional separation
- Update module-system.md with widget_provider and import rules

This enables modules to provide rich dashboard widgets without core modules
importing from optional modules, maintaining true module independence.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 19:01:23 +01:00

12 KiB

Cross-Module Import Rules

This document defines the strict import rules that ensure the module system remains decoupled, testable, and resilient. These rules are critical for maintaining a truly modular architecture.

Core Principle

Core modules NEVER import from optional modules.

This is the fundamental rule that enables optional modules to be truly optional. When a core module imports from an optional module:

  • The app crashes if that module is disabled
  • You can't test core functionality in isolation
  • You create a hidden dependency that violates the architecture

Module Classification

Core Modules (Always Enabled)

  • contracts - Protocols and interfaces
  • core - Dashboard, settings, profile
  • tenancy - Platform, company, vendor, admin user management
  • cms - Content pages, media library
  • customers - Customer database
  • billing - Subscriptions, tier limits
  • payments - Payment gateway integrations
  • messaging - Email, notifications

Optional Modules (Per-Platform)

  • analytics - Reports, dashboards
  • cart - Shopping cart
  • catalog - Product browsing
  • checkout - Cart-to-order conversion
  • inventory - Stock management
  • loyalty - Loyalty programs
  • marketplace - Letzshop integration
  • orders - Order management

Import Rules Matrix

From \ To Core Optional Contracts
Core FORBIDDEN
Optional ⚠️ With care
Contracts

Explanation

  1. Core → Core: Allowed. Core modules can import from each other (e.g., billing imports from tenancy)

  2. Core → Optional: FORBIDDEN. This is the most important rule. Core modules must never have direct imports from optional modules.

  3. Core → Contracts: Allowed. Contracts define shared protocols and data structures.

  4. Optional → Core: Allowed. Optional modules can use core functionality.

  5. Optional → Optional: Allowed with care. Check dependencies in definition.py to ensure proper ordering.

  6. Optional → Contracts: Allowed. This is how optional modules implement protocols.

  7. Contracts → Anything: Contracts should only depend on stdlib/typing/Protocol. No module imports.

Anti-Patterns (DO NOT DO)

Direct Import from Optional Module

# app/modules/core/routes/api/admin_dashboard.py

# BAD: Core importing from optional module
from app.modules.marketplace.services import marketplace_service
from app.modules.analytics.services import stats_service

@router.get("/dashboard")
def get_dashboard():
    # This crashes if marketplace is disabled!
    imports = marketplace_service.get_recent_imports()

Conditional Import with ismodule_enabled Check

# BAD: Still creates import-time dependency
from app.modules.service import module_service

if module_service.is_module_enabled(db, platform_id, "marketplace"):
    from app.modules.marketplace.services import marketplace_service  # Still loaded!
    imports = marketplace_service.get_recent_imports()

Type Hints from Optional Modules

# BAD: Type hint forces import
from app.modules.marketplace.models import MarketplaceImportJob

def process_import(job: MarketplaceImportJob) -> None:  # Crashes if disabled
    ...

Approved Patterns

1. Provider Protocol Pattern (Metrics & Widgets)

Use the provider protocol pattern for cross-module data:

# app/modules/core/services/stats_aggregator.py

# GOOD: Import only protocols and contracts
from app.modules.contracts.metrics import MetricsProviderProtocol, MetricValue

class StatsAggregatorService:
    def _get_enabled_providers(self, db, platform_id):
        # Discover providers from MODULES registry
        from app.modules.registry import MODULES

        for module in MODULES.values():
            if module.has_metrics_provider():
                # Provider is called through protocol, not direct import
                provider = module.get_metrics_provider_instance()
                yield provider

2. Context Provider Pattern

Modules contribute context without direct imports:

# app/modules/core/utils.py

def get_context_for_frontend(frontend_type, request, db, platform):
    # GOOD: Discover and call providers through registry
    for module in get_enabled_modules(db, platform.id):
        if module.has_context_provider(frontend_type):
            context.update(module.get_context_contribution(...))

3. Lazy Factory Functions

Use factory functions in module definitions:

# app/modules/marketplace/definition.py

def _get_widget_provider():
    """Lazy import to avoid circular imports."""
    from app.modules.marketplace.services.marketplace_widgets import marketplace_widget_provider
    return marketplace_widget_provider

marketplace_module = ModuleDefinition(
    code="marketplace",
    widget_provider=_get_widget_provider,  # Called lazily when needed
)

4. Type Checking Only Imports

Use TYPE_CHECKING for type hints without runtime dependency:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from app.modules.marketplace.models import MarketplaceImportJob

def process_import(job: "MarketplaceImportJob") -> None:
    # Works at runtime even if marketplace is disabled
    ...

5. Registry-Based Discovery

Discover modules through the registry, not imports:

# GOOD: Discovery through registry
from app.modules.registry import MODULES

def get_module_if_enabled(db, platform_id, module_code):
    module = MODULES.get(module_code)
    if module and module_service.is_module_enabled(db, platform_id, module_code):
        return module
    return None

Architecture Validation

The architecture validator (scripts/validate_architecture.py) includes rules to detect violations:

Rule Severity Description
IMPORT-001 ERROR Core module imports from optional module
IMPORT-002 WARNING Optional module imports from unrelated optional module
IMPORT-003 INFO Consider using protocol pattern instead of direct import

Run validation:

python scripts/validate_architecture.py

Provider Pattern Summary

When to Use Each Pattern

Need Pattern Location
Numeric statistics MetricsProvider contracts/metrics.py
Dashboard widgets WidgetProvider contracts/widgets.py
Page template context Context Provider definition.py
Service access Protocol + DI contracts/{module}.py

Provider Flow

┌─────────────────────────────────────────────────────────────────────┐
│                         CONTRACTS LAYER                              │
│                     (Shared protocol definitions)                    │
│                                                                      │
│   MetricsProviderProtocol    DashboardWidgetProviderProtocol        │
│   ContentServiceProtocol     ServiceProtocol                         │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                    ┌───────────────┼───────────────┐
                    ▼               ▼               ▼
        ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
        │  CORE MODULE     │ │ OPTIONAL MODULE 1│ │ OPTIONAL MODULE 2│
        │                  │ │                  │ │                  │
        │ StatsAggregator  │ │ OrderMetrics     │ │ CatalogMetrics   │
        │ WidgetAggregator │ │ (implements)     │ │ (implements)     │
        │ (discovers)      │ │                  │ │                  │
        └──────────────────┘ └──────────────────┘ └──────────────────┘
                │                       │                    │
                │                       │                    │
                ▼                       ▼                    ▼
        ┌──────────────────────────────────────────────────────────────┐
        │                      MODULE REGISTRY                          │
        │                                                               │
        │  MODULES = {                                                  │
        │      "orders": ModuleDefinition(metrics_provider=...),        │
        │      "catalog": ModuleDefinition(metrics_provider=...),       │
        │  }                                                            │
        └──────────────────────────────────────────────────────────────┘

Testing Without Dependencies

Because core doesn't depend on optional modules, you can test core in isolation:

# tests/unit/core/test_stats_aggregator.py

from unittest.mock import MagicMock
from app.modules.contracts.metrics import MetricValue, MetricsProviderProtocol

class MockMetricsProvider:
    """Mock that implements the protocol without needing real modules."""
    @property
    def metrics_category(self) -> str:
        return "test"

    def get_vendor_metrics(self, db, vendor_id, context=None):
        return [MetricValue(key="test.value", value=42, label="Test", category="test")]

def test_stats_aggregator_with_mock_provider():
    # Test core without any optional modules
    mock_provider = MockMetricsProvider()
    ...

Consequences of Violations

If Core Imports Optional Module

  1. Import Error: App fails to start if module code has syntax errors
  2. Runtime Crash: App crashes when disabled module is accessed
  3. Hidden Dependency: Module appears optional but isn't
  4. Testing Difficulty: Can't test core without all modules
  5. Deployment Issues: Can't deploy minimal configurations

Detection

Add these checks to CI:

# Check for forbidden imports
grep -r "from app.modules.marketplace" app/modules/core/ && exit 1
grep -r "from app.modules.analytics" app/modules/core/ && exit 1
grep -r "from app.modules.orders" app/modules/core/ && exit 1

Summary

Rule Enforcement
Core → Optional = FORBIDDEN Architecture validation, CI checks
Use Protocol pattern Code review, documentation
Lazy factory functions Required for definition.py
TYPE_CHECKING imports Required for type hints across modules
Registry-based discovery Required for all cross-module access

Following these rules ensures:

  • Modules can be truly enabled/disabled per platform
  • Testing can be done in isolation
  • New modules can be added without modifying core
  • The app remains stable when modules fail