skills/psincraian/myfy/module-development

module-development

SKILL.md

Module Development in myfy

Modules are the building blocks of myfy applications. Each module follows a protocol with lifecycle hooks.

Available Modules

myfy provides these built-in modules:

Module Package Purpose
WebModule myfy.web HTTP routing, ASGI, FastAPI-like decorators
DataModule myfy.data SQLAlchemy async, migrations, sessions
FrontendModule myfy.frontend Jinja2, Tailwind 4, DaisyUI 5, Vite
TasksModule myfy.tasks Background jobs, SQL-based task queue
UserModule myfy.user Auth, OAuth, user management
CliModule myfy.commands Custom CLI commands
AuthModule myfy.web.auth Type-based authentication, protected routes
RateLimitModule myfy.web.ratelimit Rate limiting per IP or user

Module Protocol

from myfy.core import BaseModule, Container, SINGLETON

class MyModule(BaseModule):
    """Custom module following the Module protocol."""

    def __init__(self):
        super().__init__(name="my-module")

    @property
    def requires(self) -> list[type]:
        """Module types this module depends on."""
        return []  # e.g., [WebModule, DataModule]

    @property
    def provides(self) -> list[type]:
        """Extension protocols this module implements."""
        return []  # e.g., [IWebExtension]

    def configure(self, container: Container) -> None:
        """Phase 1: Register providers in DI container."""
        container.register(
            type_=MyService,
            factory=lambda: MyService(),
            scope=SINGLETON,
        )

    def extend(self, container: Container) -> None:
        """Phase 2: Modify other modules' registrations (optional)."""
        pass

    def finalize(self, container: Container) -> None:
        """Phase 3: Configure singletons after compilation (optional)."""
        # Safe to get singletons here - container is compiled
        service = container.get(MyService)
        service.setup()

    async def start(self) -> None:
        """Phase 4: Start runtime services (connect to DB, etc.)."""
        pass

    async def stop(self) -> None:
        """Phase 5: Cleanup resources gracefully."""
        pass

Lifecycle Phases

  1. Discovery - Application discovers all modules
  2. Dependency Validation - Validates module dependency graph
  3. Configure - Each module registers services in DI
  4. Extend - Modules can modify other modules' registrations
  5. Compile - DI container builds injection plans
  6. Finalize - Modules configure singleton services
  7. Start - Runtime services start (in dependency order)
  8. Stop - Graceful shutdown (reverse order)

Module Dependencies

Declare dependencies via the requires property:

from myfy.web import WebModule
from myfy.data import DataModule

class MyModule(BaseModule):
    @property
    def requires(self) -> list[type]:
        # This module needs WebModule and DataModule to be registered
        return [WebModule, DataModule]

The application validates all required modules are present and initializes them in topological order.

Registering Settings

Modules typically register their own settings:

from myfy.core.config import load_settings
from myfy.core.di.types import ProviderKey

class MyModule(BaseModule):
    def __init__(self, settings: MySettings | None = None):
        super().__init__("my-module")
        self._settings = settings

    def configure(self, container: Container) -> None:
        # Check if settings already registered (avoid double registration)
        key = ProviderKey(MySettings)
        if key not in container._providers:
            if self._settings is None:
                self._settings = load_settings(MySettings)
            container.register(
                type_=MySettings,
                factory=lambda: self._settings,
                scope=SINGLETON,
            )

Extension Protocols

Define extension interfaces using provides:

from myfy.web.extensions import IWebExtension

class MyModule(BaseModule):
    @property
    def provides(self) -> list[type]:
        return [IWebExtension]

    def finalize(self, container: Container) -> None:
        # Access ASGIApp after compilation
        asgi_app = container.get(ASGIApp)
        asgi_app.app.mount("/my-path", MyASGIApp())

Complete Module Example

"""
MyFeature module for myfy.

Provides feature X with Y capabilities.
"""
from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from myfy.core import BaseModule, SINGLETON
from myfy.core.config import load_settings
from myfy.core.di.types import ProviderKey

from .config import MyFeatureSettings
from .service import MyFeatureService

if TYPE_CHECKING:
    from myfy.core.di import Container

logger = logging.getLogger(__name__)


class MyFeatureModule(BaseModule):
    """
    MyFeature module.

    Features:
    - Feature capability 1
    - Feature capability 2
    """

    def __init__(self, settings: MyFeatureSettings | None = None):
        super().__init__("my-feature")
        self._settings = settings

    @property
    def requires(self) -> list[type]:
        return []

    @property
    def provides(self) -> list[type]:
        return []

    def configure(self, container: Container) -> None:
        logger.debug("Configuring MyFeatureModule...")

        # Register settings
        key = ProviderKey(MyFeatureSettings)
        if key not in container._providers:
            if self._settings is None:
                self._settings = load_settings(MyFeatureSettings)
            container.register(
                type_=MyFeatureSettings,
                factory=lambda: self._settings,
                scope=SINGLETON,
            )

        # Register services
        container.register(
            type_=MyFeatureService,
            factory=lambda settings=self._settings: MyFeatureService(settings),
            scope=SINGLETON,
        )

        logger.debug("MyFeatureModule configured")

    async def start(self) -> None:
        logger.info("MyFeature module started")

    async def stop(self) -> None:
        logger.info("MyFeature module stopped")


# Module instance for entry point discovery
my_feature_module = MyFeatureModule()

Best Practices

  1. Always call super().init(name) - Required by BaseModule
  2. Use logging - Add debug/info logs for troubleshooting
  3. Allow settings injection - Accept optional settings in init for testing
  4. Check for existing registrations - Avoid double registration errors
  5. Keep configure() pure - Only register providers, no side effects
  6. Use finalize() for singletons - Safe to resolve singletons here
  7. Make start/stop idempotent - Safe to call multiple times
  8. Include docstrings - Document what the module provides
Weekly Installs
2
Repository
psincraian/myfy
GitHub Stars
86
First Seen
14 days ago
Installed on
gemini-cli2
opencode2
codebuddy2
github-copilot2
codex2
kimi-cli2