FundamentalsModules
Fundamentals~ 9 min read

Modules

Modules are a powerful and central concept in Flama designed for organising and extending the framework's capabilities. They serve as dedicated units for managing related functionalities, configurations, and resources, particularly those with a lifecycle tied to the application itself. Modules are key to Flama's extensibility, enabling the integration of external tools and services, such as databases like SQLAlchemy, or MLOps platforms like MLFlow, effectively allowing you to create sophisticated plug-ins and deeply customise your application's behaviour.

What are modules?

In Flama, a Module is a class that groups together specific functionalities, often managing the setup and teardown of services or resources during the application's lifespan. They provide hooks into the application's startup and shutdown events, making them ideal for initialising connections, loading configurations, or registering other Flama constructs like Components or routes dynamically. Think of them as self-contained packages of functionality that enhance your core application.

Why are they important?

  1. Extensibility and Customisation: This is the primary virtue of modules. They provide a clean mechanism to build plug-ins or integrations with other libraries and services. For instance, a module could handle all aspects of database interaction using SQLAlchemy, or integrate with a machine learning experiment tracking tool like MLFlow, encapsulating all related logic. This makes Flama highly adaptable to diverse project needs.
  2. Lifecycle Management: Modules can execute code during application startup (e.g., to initialise database connections, load machine learning models, or warm up caches) and shutdown (e.g., to gracefully close connections or save state).
  3. Organisation and Encapsulation: They help in structuring larger applications by encapsulating related pieces of functionality (services, configurations, components, routes) into manageable and reusable units, keeping your main application code cleaner.
  4. Service Provision: Modules can initialise and manage services that can then be made available to other parts of your application, including route handlers or other modules. This shared-service capability is a significant advantage for application-wide utilities.

The main virtue Modules bring is their ability to seamlessly extend Flama's core, offering a structured way to introduce significant new features, manage complex initialisation and cleanup processes, and integrate third-party systems, making them a game changer for building sophisticated and tailored applications.

Building a module

How to

Creating a custom module in Flama generally involves these steps:

  1. Inherit from flama.modules.Module: Your custom module class must inherit from the base flama.modules.Module class.

    from flama.modules import Module
    class MyCustomModule(Module): pass
  2. Define a name (optional but recommended): You can assign a string to the name class attribute. If set, this name is used to make the module instance directly accessible as an attribute on the Flama application instance (e.g., if name="custom", you can access it via app.custom). It also serves as a key in the app.modules dictionary.

    class MyCustomModule(Module):    name = "custom"    # ... rest of the module definition
  3. Implement lifecycle hooks (if needed): Modules can define on_startup() and on_shutdown() asynchronous methods to hook into the application's lifecycle.

  4. Accessing the application instance: Once a module is registered with a Flama application, its self.app attribute will reference the application instance. This allows the module to interact with the application, for example, to add components or routes.

Lifecycle hooks

Modules can participate in the application's lifecycle through two primary asynchronous methods:

  • on_startup: This method is called automatically when the Flama application starts up. It is the ideal place for:

    • Initialising resources like database connections or HTTP clients.
    • Loading configurations or pre-trained machine learning models.
    • Setting up any state that the module will manage during the application's life.
    • Programmatically adding Components or routes to the application via self.app.
  • on_shutdown: This method is called automatically when the Flama application is shutting down. It should be used for:

    • Releasing resources cleanly (e.g., closing database connections).
    • Saving any persistent state if necessary.
    • Performing any other cleanup tasks required by the module.

Providing services and extending functionality

One of the primary roles of a module is to initialise, configure, and provide access to services or shared objects.

  • A module can initialise service instances within its __init__() or, more commonly, in its on_startup() method.
  • It can then expose these services through public methods (e.g., get_service()) or attributes, or even direct functional methods on the module itself.
  • For deeper integration with Flama's dependency injection system, a module can also programmatically register Components during its startup phase. These components can then provide the module's services to route handlers and other parts of the application that support dependency injection. This is a powerful pattern for making module-managed services easily consumable and is a key aspect of their "game changer" status for extensibility.

Utilising modules

After defining your module, you need to register it with your Flama application and then access it.

Registering modules

You register modules by passing a list of module instances to the modules parameter when creating your Flama application instance.

from flama import Flamafrom .my_i18n_module import I18nModule # Assuming I18nModule is defined
translations = {"en": {"greeting": "Hello"}, "es": {"greeting": "Hola"}}i18n_module_instance = I18nModule(translations_data=translations, default_lang="en")app = Flama(modules=[i18n_module_instance])

When the application starts, it will iterate through the registered modules, set their app attribute, and call their on_startup() methods. Similarly, on shutdown, their on_shutdown() methods will be called.

Accessing modules

Once registered, you can access module instances in a couple of ways:

  1. By name (if the name attribute is set): If your module has a name attribute (e.g., name="i18n"), you can access it directly as an attribute on the application instance:

    # Assuming 'app' is your Flama application instance and I18nModule has name = "i18n"translated_string = app.i18n.translate("greeting_key", lang="es")
  2. Via app.modules: All registered modules are also available in the app.modules collection (which behaves like a dictionary). You can access them using the module's name (if set).

    # Access by name:i18n_module_by_name = app.modules["i18n"]

Example

Let's create a more illustrative example: an I18nModule for handling internationalisation. This module will load a set of translations at startup and provide a method to retrieve translated strings, which can be used by any route in the application. This demonstrates a shared, application-level utility managed by a module.

import typing as t
import flamafrom flama import Flamafrom flama.modules import Modulefrom flama.http import PlainTextResponse

# 1. Define the Internationalisation Serviceclass I18nService: """A simple service for providing translated messages."""
def __init__(self, translations: t.Dict[str, t.Dict[str, str]], default_lang: str): self._translations = translations self._default_lang = default_lang
def translate( self, key: str, lang: str, default_message: t.Optional[str] = None ) -> str: """ Retrieves a translated string for the given key and language. Falls back to default_message or the key itself if not found. """ if default_message is None: default_message = key # Fallback to the key if no other default is provided
return self._translations.get(lang, {}).get(key, default_message)

# 2. Define the Internationalisation Moduleclass I18nModule(Module): """ A Module for managing internationalisation. This module initialises an I18nService with translations and a default language. """
name = "i18n" # Allows access via app.i18n
def __init__( self, translations_data: t.Dict[str, t.Dict[str, str]], default_lang: str = "en" ): super().__init__() self._translations_data = translations_data self._default_lang = default_lang self.service: t.Optional[I18nService] = None
async def on_startup(self): """Initialises the I18nService when the application starts.""" self.service = I18nService( translations=self._translations_data, default_lang=self._default_lang )
# Advanced: Registering the service or a translate function as a Component # This would allow route handlers to get translations via dependency injection. # if self.app and self.service: # service_instance = self.service # class _I18nServiceComponent(Component): # def resolve(self) -> I18nService: # type: ignore[valid-type] # return service_instance # Provide the module-managed service instance # # self.app.add_component(_I18nServiceComponent()) # # # Or, to provide the translate method directly as a component: # # def get_translate_func(): # Factory function # # def translate_wrapper(key: str, lang: t.Optional[str] = None, default: t.Optional[str] = None) -> str: # # return self.translate(key, lang, default_message=default) # # return translate_wrapper # # # # class _TranslateComponent(Component): # # def resolve(self) -> t.Callable: # type: ignore[valid-type] # # return get_translate_func() # # self.app.add_component(_TranslateComponent())
async def on_shutdown(self): """Performs cleanup for the I18nService.""" self.service = None # Simple cleanup for this example
def translate( self, key: str, lang: t.Optional[str] = None, default_message: t.Optional[str] = None, ) -> str: """ Provides a convenient way to get translated strings. Uses the specified language or the module's default language. """ if not self.service: raise RuntimeError( "I18nService is not initialised. Ensure the application has started." )
target_lang = lang if lang else self._default_lang
# If no specific default_message is passed to this method, # we let the underlying service use its own fallback logic (which might be the key itself). return self.service.translate(key, target_lang, default_message=default_message)

# 3. Define sample translation datasample_translations = { "en": { "welcome": "Welcome to our amazing application!", "farewell": "Goodbye and thank you for visiting!", "item_info": "This item is named '{item_name}'.", }, "es": { "welcome": "¡Bienvenido/a a nuestra increíble aplicación!", "farewell": "¡Adiós y gracias por su visita!", "item_info": "Este artículo se llama '{item_name}'.", }, "fr": { "welcome": "Bienvenue sur notre application incroyable !", # farewell in French not provided, will fall back },}
# 4. Application setup# Initialise the Flama application and register the I18nModule instanceapp = Flama( modules=[I18nModule(translations_data=sample_translations, default_lang="en")])

# 5. Define routes that will use the internationalisation service from the module@app.route("/home")async def home_page(): # Access the module via its registered name on the app object # t.cast is for static type checking benefits i18n_module = t.cast(I18nModule, app.i18n) welcome_message = i18n_module.translate("welcome") return PlainTextResponse(welcome_message)

@app.route("/home/{lang_code}")async def home_page_localised(lang_code: str): i18n_module = t.cast(I18nModule, app.i18n) welcome_message = i18n_module.translate("welcome", lang=lang_code) return PlainTextResponse(welcome_message)

@app.route("/item/{item_id}")async def item_details( item_id: str, lang: t.Optional[str] = None): # lang from query param i18n_module = t.cast(I18nModule, app.i18n) item_name_for_translation = f"Item {item_id}" item_message = i18n_module.translate("item_info", lang=lang).format( item_name=item_name_for_translation ) farewell_message = i18n_module.translate( "farewell", lang=lang, default_message="Come back soon!" ) return PlainTextResponse(f"{item_message}\n{farewell_message}")

# This part allows the example to be run directly if saved as a .py file.if __name__ == "__main__": flama.run(flama_app=app, server_host="0.0.0.0", server_port=8000)

This I18nModule example effectively shows:

  • How a module can initialise and provide an application-wide service (the I18nService)
  • Routes can then easily access this service through the registered module instance on the app object (e.g., app.i18n) to perform tasks like translating text based on language preferences.
  • The example also includes fallback mechanisms for missing translations and demonstrates how the module itself can offer convenient wrapper methods (like I18nModule.translate())
  • The commented-out lines in the on_startup() method further hint at how such a module could register Components to make its services available through <FlamaName />'s dependency injection, further enhancing decoupling and testability, which is a key aspect of <FlamaName />'s power and extensibility