A module is a class annotated with a @Module() decorator.
The @Module() decorator provides metadata that exports a Module data and defines the module structure.
The best way to organize your components is to build your projects as Modules.
The ApplicationModule is the entry-point/root module to building the application module tree -
the internal data structure used to resolve module and provider relationships and dependencies.
Thus, the architecture resulting from most applications will include multiple modules with closely related functionality.
Building an application as a group of feature modules bundled together helps to manage complexity, have a maintainable, extendable, and testable code base, and encourage development using SOLID principles.
A typical example of a feature module is the car project. The CarModule wraps all the services and controller that manages the car resource which makes it easy to maintain, extend, and testable.
from ellar.common import Module
from ellar.core import ModuleBase
from ellar.di import Container
from .services import CarRepository
from .controllers import CarController
@Module(
controllers=[CarController],
providers=[CarRepository],
)
class CarModule(ModuleBase):
def register_providers(self, container: Container) -> None:
# for more complicated provider registrations
# container.register_instance(...)
passLet's create a Module and take a quick overview of its parameters.
from ellar.common import Module
from ellar.core import ModuleBase
@Module(
name='',
modules=[],
providers=[],
controllers=[],
routers=[],
commands=[],
base_directory=None,
static_folder='static',
template_folder='templates'
)
class BookModule(ModuleBase):
passname |
name of the module - it's irrelevant at the moment. |
modules |
List of Module dependencies |
providers |
the providers that will be instantiated by the Ellar injector and that may be shared at least across this module |
controllers |
the set of controllers defined in this module which have to be instantiated |
routers |
the set of ModuleRouter defined in this module |
commands |
the set of EllarTyper or command decorated functions |
base_directory |
root directory for this module to read static_folder and template_folder. Default is the root folder of the Module Class |
static_folder |
defines the static folder for this module - default: static |
template_folder |
defines the template folder for this module - default: templates |
Every registered Module receives two event calls during its instantiation and when the application is ready.
from ellar.common import Module
from ellar.core import ModuleBase, Config
@Module()
class ModuleEventSample(ModuleBase):
@classmethod
def before_init(cls, config: Config) -> None:
"""Called before creating Module object"""before_init receives current app Config as a parameter for further configurations before ModuleEventSample is initiated.
It's important to note that returned values from before_init will be passed to the constructor of ModuleEventSample during instantiation.
Custom exception handlers can be registered through modules.
from ellar.common import Module, exception_handler, JSONResponse, Response, IHostContext
from ellar.core import ModuleBase
@Module()
class ModuleExceptionSample(ModuleBase):
@exception_handler(404)
def exception_404_handler(cls, context: IHostContext, exc: Exception) -> Response:
return JSONResponse(dict(detail="Resource not found."))exception_404_handler will be register to the application at runtime during ModuleExceptionSample computation.
We can also define Jinja2 templating filters in project Modules or any @Module() module.
The defined filters are be passed down to Jinja2 environment instance alongside the template_folder
value when creating TemplateLoader.
from ellar.common import Module, template_global, template_filter
from ellar.core import ModuleBase
@Module()
class ModuleTemplateFilterSample(ModuleBase):
@template_filter()
def double_filter(cls, n):
return n * 2
@template_global()
def double_global(cls, n):
return n * 2
@template_filter(name="dec_filter")
def double_filter_dec(cls, n):
return n * 2A module class can inject providers as well (e.g., for configuration purposes):
For example, from our sample project, we can inject Config to the CarModule
# project_name/apps/car/module.py
from ellar.common import Module
from ellar.core import Config, ModuleBase
from ellar.di import Container
from .services import CarRepository
from .controllers import CarController
@Module(
controllers=[CarController],
providers=[CarRepository],
)
class CarModule(ModuleBase):
def __init__(self, config: Config):
self.config = config
def register_providers(self, container: Container) -> None:
# for more complicated provider registrations
# container.register_instance(...)
passMiddlewares functions can be defined at Module level with @middleware() function decorator.
For example:
from ellar.common import Module, middleware, IHostContext, PlainTextResponse
from ellar.core import ModuleBase
@Module()
class ModuleMiddlewareSample(ModuleBase):
@middleware()
async def my_middleware_function_1(cls, context: IHostContext, call_next):
request = context.switch_to_http_connection().get_request() # for http response only
request.state.my_middleware_function_1 = True
await call_next()
@middleware()
async def my_middleware_function_2(cls, context: IHostContext, call_next):
if context.get_type() == 'websocket':
websocket = context.switch_to_websocket().get_client()
websocket.state.my_middleware_function_2 = True
await call_next()
@middleware()
async def my_middleware_function_3(cls, context: IHostContext, call_next):
connection = context.switch_to_http_connection().get_client() # for http response only
if connection.headers['somekey']:
# response = context.get_response() -> use the `response` to add extra definitions to things you want to see on
return PlainTextResponse('Header is not allowed.')
await call_next()Things to note:
- middleware functions must be
async. - middleware functions can return a
responseor modify aresponsereturned - middleware functions must call
call_nextandawaitits actions as shown above.
EllarInjector is based on a python library injector. Both share similar Module features with few distinct features.
As an added support, you can create or reuse modules from injector Modules.
!!! info
This type of module is used to configure injector bindings and providers for dependency injection purposes.
from ellar.core import ModuleBase
from ellar.di import Container
from injector import provider
class Name(str):
pass
class Description(str):
pass
class ExampleModule(ModuleBase):
def register_services(self, container: Container) -> None:
container.bind(Name, to='Sherlock')
@provider
def describe(self, name: Name) -> Description:
return Description('%s is a man of astounding insight' % name)The ExampleModule has registered Description and Name type to the injector and can be resolved respectively if required by any object.
Read more on injector module use cases - Here.
