Custom Decorators¶
Ellar provides a variety of function decorators in the ellar.common
python module that can be used to modify the behavior of route functions.
These decorators can be used to change the response type of a route function, add filters to the response schema, define the OPENAPI context, and more. In general, these decorators can help to simplify and streamline the process of creating routes.
HTTP Method Decorator¶
@get
, @post
, @put
, @patch
, @delete
, @trace
, @options
, @head
are decorators that define the standard HTTP methods for a route function. They indicate the type of HTTP request that the route function can handle, such as a GET
request or a POST
request.
@http_route
is a decorator that can be used to define a route that can handle multiple HTTP methods at once.
@ws_route
is a decorator that is used to define a route that can handle WebSocket connections.
These decorators help to specify which type of request a route function can handle.
Route Function Parameters¶
These are Pydantic Models that defines and validates user input dependencies as a route function parameters. They are used to ensure that the specified parameters are present in the request and are of the correct type.
If any of the specified parameters are missing or are of an invalid type, the decorators will raise a 422
error code and also provide a clear error message if the input validation fails. This helps to ensure that your application is receiving valid input and can process the request correctly.
Body[T, Body.P(..., embed=False)]
: marks a parameter as a field to be available in a request body ofapplication/json
content-type.Form[T, Form.P(..., embed=True)]
: marks a parameter as a field to be available in a request body ofx-www-form-urlencoded or multipart/form-data
content-type.Query[T, Query.P(...)]
: marks a parameter as a field to be available in request query objectFile[T, File.P(...)]
: marks a parameter as a field to be available in a request body ofmultipart/form-data
content-type.Path[T, Path.P(...)]
: marks a parameter as a request path variable.Header[T, Header.P(...)]
: marks a parameter as a request header variable.Cookie[T, Cookie.P(...)]
: marks a parameter as a request cookie variable.WsBody[T, WsBody.P(...)]
: defines the message format that should be transmitted from the client in aWebSocket
when there is a successful connection. See websocket advance doc
Please refer to the "How-to-Guide" on parsing inputs here to see how this input decorators work.
Non Route Function Parameters¶
We discussed decorators that are used to define route function parameter dependencies in Ellar. These decorators, such as Query
, Form
, and Body
, etc. are pydantic models used to specify the expected parameters for a route function.
However, there are also some route parameters that are system dependent, such as the request
or websocket
object, and the response
object. These parameters are resolved by the application and supplied to the route function when needed, and are not specified with pydantic models or user input.
Inject[Type]¶
The Inject[Type] annotation is used to inject a service registered in Ellar DI container and also inject system services into a route function parameter.
For example:
from ellar.app import App
from ellar.common import ModuleRouter, Inject
from ellar.core import Config
from sqlalchemy.ext.asyncio import AsyncSession
router = ModuleRouter('/test-provide')
@router.get('/app')
def example_endpoint(app: Inject[App], config: Inject[Config], async_session: Inject[AsyncSession]):
assert isinstance(app, App)
assert app.config == config
assert isinstance(config, Config)
return {'message': 'injected App and Configuration object to route function'}
example_endpoint
function has two parameters app
and config
which are annotated with Inject[Type]
. This annotation tells the application to resolve the App
and Config
service providers and inject them as the app
and config
parameters when the endpoint is called. This allows for easy access to the objects without having to manually import and instantiate them. It also makes the code more modular and easier to test.
Info
Only types registered in the application can be resolved, but you can set INJECTOR_AUTO_BIND = True
in configuration for the injector to register automatically that are not found. please note that this automatic registration will be scoped to singleton
by the EllarInjector
.
Injecting ExecutionContext¶
We inject ExecutionContext
by simply annotating a parameter with Inject and IExecutionContext interface or ExecutionContext class. See ExecutionContext for more understanding of ExecutionContext
For example:
from ellar.common import ModuleRouter, IExecutionContext, Inject
router = ModuleRouter('/test-context')
@router.get('/')
def example_endpoint(ctx: Inject[IExecutionContext]):
http_connection_instance = ctx.switch_to_http_connection().get_client()
query_params = http_connection_instance.query_params
return {'message': 'inject execution context', 'query_params': query_params}
Injecting Request Object¶
We inject Request
by simply annotating a parameter with Inject and Request class.
For example:
from ellar.common import ModuleRouter, Inject
from ellar.core import Request
router = ModuleRouter('/test-req')
@router.get('/')
def example_endpoint(req: Inject[Request]):
headers = req.headers
query_params = req.query_params
return {'message': 'injected request object', 'headers': headers, 'query_params': query_params}
In this example, the example_endpoint
function has a parameter annotated with Inject[Request]
, which will be automatically populated with the current Request
object at runtime. The headers
and query_params
attributes of the req
object can then be accessed and used within the function.
Injecting Response Object¶
We inject Response
by simply annotating a parameter with Inject and Response class.
For example:
from ellar.common import ModuleRouter, Inject, Response
router = ModuleRouter('/test-response')
@router.get('/')
def example_endpoint(res: Inject[Response]):
res.headers['x-custom-header'] = 'hello'
return {'message': 'inject response object'}
In this example, the Inject[Response]
annotation injects the current Response
object to the res
parameter of the example_endpoint
function. This will allow you to manipulate the headers of the response before it is sent back to the client.
Injecting Websocket Object¶
We inject Websocket
by simply annotating a parameter with Inject and Websocket class.
For example:
from ellar.common import ModuleRouter, Inject
from ellar.core import WebSocket
router = ModuleRouter('/test-ws')
@router.ws_route('/')
async def example_endpoint(ws: Inject[WebSocket]):
await ws.accept()
await ws.send_json({'message': 'injected WebSocket object to route function'})
example_endpoint
function is executed. The Ws
decorator injects the current WebSocket
object to the ws
parameter of the function, which can then be used to interact with the WebSocket connection, such as accepting the connection and sending data to the client. The same conditions and examples applies for:
- Host
Inject[str, Inject.Key("Host")]
injects current client host address to route function parameter. see example below - Session
Inject[dict, Inject.Key("Session")]
injects current Session object to route function parameter. This requires SessionMiddleware module from Starlette added in application middleware and alsoSessionMiddleware
module depends on itsdangerous package. - Http
Inject[dict, Inject.Key("Session")]
injects current HTTP connection object to route function parameter. see example below
For example:
from ellar.common import Inject, ModuleRouter
from starlette.requests import HTTPConnection
router = ModuleRouter()
@router.get("/others")
def get_requests_case_2(
session: Inject[dict, Inject.Key("Session")], # injects requests session
host: Inject[str, Inject.Key("Host")], # injects requests host
connection: Inject[HTTPConnection], # injects connection
) -> bool:
assert isinstance(connection, HTTPConnection) # True
assert host == "testclient"
assert isinstance(session, dict) and len(session) == 0
return True
Custom Parameter Decorators¶
You can create your own route parameter decorators whenever necessary. You simply need to follow a contract, NonParameterResolver
, and override the resolve function.
The NonParameterResolver
has two attributes, type_annotation
and parameter_name
, that are provided automatically when computing route function parameter dependencies. The type_annotation
and parameter_name
are determined from the parameter declaration like so - def route_function(parameter_name:type_annotation = NonParameterResolver())
.
All NonParameterResolver
receives current IExecutionContext
during route function execution, and it must return a tuple of dict object of the resulting resolve data with parameter_name
and list of errors if any. As shown in the return statements in the example below.
For example:
import typing as t
from ellar.common.params import SystemParameterResolver
from ellar.common import IExecutionContext
from pydantic.error_wrappers import ErrorWrapper
class UserParam(SystemParameterResolver):
async def resolve(self, ctx: IExecutionContext, **kwargs: t.Any) -> t.Any:
request = ctx.switch_to_http_connection().get_request()
user = request.get('user', None)
if user:
return {self.parameter_name: user}, []
return {}, [ErrorWrapper('Authenticated Users Only', loc='system')]
This example defines a custom decorator called UserParam
that inherits from NonParameterResolver
. The resolve
method is overridden to extract the user from the current IExecutionContext
's request.
If the user is found, it is returned as a dict with the key as the parameter_name
of the decorator, along with an empty list of errors. If no user is found, an empty dict and a list of errors containing an ErrorWrapper object is returned.
This UserParam
decorator can then be used to inject the user object to a route function parameter like so:
from typing_extensions import Annotated
from .custom_decorator import UserParam
from .schema import UserType
@router.get('/user')
def example_endpoint(user = UserParam(), user2: Annotated[UserType, UserParam()]):
assert user2 == user # True
return {'message': 'injected user object to route function', 'user': user}
Route Function Decorators¶
These decorators are used to modify the output data of a route function, add filtering to the output schema, or add extra OPENAPI information about the route function.
They include:
RENDER¶
@render() decorator converts a route function response to HTML template response.
for example:
from ellar.common import get, render
...
@get('/index-template')
@render(template_name='my_template')
def index(self):
return {'name': 'Ellar Template'}
In the example, the index function is decorated with the render
decorator, which will return a 200 status code and HTML content from my_template.
The return object from the index function will be used as the templating context for my_template
during the template rendering process. This allows the function to pass data to the template and have it rendered with the provided context, the rendered template will be the response body.
See HTML Templating for more information on render
and HTML templating with Ellar.
FILE¶
@file() decorator converts a route function response to file or streaming response type. Based on the value of streaming
parameter, file decorator creates FileResponseModel
or StreamingResponseModel
.
FileResponseModel as file(streaming=False)¶
When streaming
parameter in @file(streaming=False)
decorator is set to False
, a FileResponseModel
is created as the response model for the decorated route function. And the route function is required to return a dictionary object that follows a FileResponseModelSchema
format:
import typing as t
from enum import Enum
from ellar.common import Serializer
class ContentDispositionType(str, Enum):
inline = "inline"
attachment = "attachment"
class FileResponseModelSchema(Serializer):
path: str
media_type: t.Optional[str] = None
filename: t.Optional[str] = None
method: t.Optional[str] = None
content_disposition_type: ContentDispositionType = ContentDispositionType.attachment
path
: This is a required key whose value defines the path to the file to attach to the response.filename
: when specified, it will be used as the attached file's filename. The default value is computed from the file referenced.content_disposition_type
: defines the content disposition type, can be either inline or attachment. The default is attachment.media_type
: states theMIME
type of the file to be attached. The default value is computed from the file referenced.method
: HTTP method, defaults:HEAD
for example:
from ellar.common import get, file
...
@get()
@file(media_type='text/html', streaming=False)
def file_download(self):
return {'path': 'path/to/file.html', 'filename': 'code.html', 'content_disposition_type': 'attachment'}
StreamingResponseModel as file(streaming=True)¶
On the other hand, when streaming
parameter in @file(streaming=True)
decorator is set to True
, a StreamingResponseModel
is created as the response model for the decorated route function. And the route function is required to return an ContentStream
. ContentStream
is an synchronous or asynchronous iterator of string or bytes. Type definition is shown below.
import typing
import asyncio
from ellar.common import get, file
Content = typing.Union[str, bytes]
SyncContentStream = typing.Iterator[Content]
AsyncContentStream = typing.AsyncIterable[Content]
ContentStream = typing.Union[AsyncContentStream, SyncContentStream]
async def slow_numbers(minimum: int, maximum: int):
yield ("<html><body><ul>")
for number in range(minimum, maximum + 1):
yield "<li>%d</li>" % number
await asyncio.sleep(0.01)
yield ("</ul></body></html>")
...
@get('/stream')
@file(media_type='text/html', streaming=True)
def file_stream(self):
# file_stream function must return ContentStream
# return {"content": slow_numbers(1, 4), "media_type": "text/html"} Controls media_type
# return {"content": slow_numbers(1, 4)} Controls sets media_type='text/html' set in @file decorator
return slow_numbers(1, 4) # This will be converted to {"content": slow_numbers(1, 4), media_type: 'text/html'} media_type set from @file decorator
OPENAPI-INFO¶
@api_info() decorator adds extra route function OPENAPI properties to route function OPENAPI documentation. They include:
Parameters:
tags
: adds more OPENAPI tags to route function OPENAPI docs.deprecated
: marks route function as deprecated. Default is falsedescriptions
: adds description to route function OPENAPI docsoperation_id
: modifies operationid for the route function OPENAPI docssummary
: adds summary to route function OPENAPI docs
For example:
from ellar.common import get
from ellar.openapi import api_info
...
@get("/open-api-info")
@api_info(
tags=['query'],
deprecated=False,
description='open api info testing',
operation_id='some-operation-id',
summary='some summary'
)
def api_info_function(self, query: str):
return f"foo bar {query}"
SERIALIZER FILTER¶
@serializer_filter() decorator provides Pydantic filtering options to decorated route function output schema.
Parameters:
include
: fields to include in the returned dictionaryexclude
: fields to exclude from the returned dictionaryby_alias
: whether field aliases should be used as keys in the returned dictionary; defaultFalse
exclude_unset
: whether fields which were not explicitly set when creating the model should be excluded from the returned dictionary; defaultFalse
.exclude_defaults
: whether fields which are equal to their default values (whether set or otherwise) should be excluded from the returned dictionary; defaultFalse
exclude_none
: whether fields which are equal to None should be excluded from the returned dictionary; defaultFalse
For example:
import typing as t
from ellar.common import serializer_filter, get, Serializer
class UserSchema(Serializer):
username: str
password: str
first_name: t.Optional[str]
last_name: t.Optional[str]
...
@get("/serialize-filter-1", response=UserSchema)
@serializer_filter(exclude_none=True, exclude={'password'})
def serialized_output_1(self):
return dict(username='python', password='secret', first_name='ellar')
serializer_filter
to filter values that are None
and also excluded password
property from been returned. See Pydantic Model Export for more examples. VERSION¶
@version() is a decorator that provides endpoint versioning for a route function. This decorator allows you to specify the version of the endpoint that the function is associated with.
Based on the versioning scheme configuration in the application, versioned route functions are called. This can be useful for maintaining backward compatibility, or for rolling out new features to different versions of an application. More information on how to use this decorator can be found in the Versioning documentation
A quick example on how to use version
decorator:
from ellar.common import post, Version
@post("/create", name='v2_v3_list')
@Version('2', '3')
async def get_item_v2_v3(self):
return {'message': 'for v2 and v3 request'}
The version
decorator takes a list of values as an argument, for example @version('2', '3')
. This indicates that the get_item_v2_v3
route function will handle version 2 and version 3 requests of the /create endpoint. This allows for multiple versions of the same endpoint to be handled by different route functions, each with their own logic and implementation.
UseGuards¶
@UseGuards() is a decorator that applies a protection class of type GuardCanActivate
to a route function. These protection classes have a can_execute
function that is called to determine whether a route function should be executed.
This decorator allows you to apply certain conditions or checks before a route function is executed, such as authentication
or authorization
checks. This can help to ensure that only authorized users can access certain resources.
More information on how to use this decorator can be found in the Guard Documentation
A quick example on how to use UseGuards
decorator:
import typing as t
from ellar.common import get, UseGuards
from ellar.core import APIKeyQuery, HTTPConnection
class MyAPIKeyQuery(APIKeyQuery):
async def authenticate(self, connection: HTTPConnection, key: t.Optional[t.Any]) -> t.Optional[t.Any]:
if key == 'supersecret':
return True
return False
@get("/")
@UseGuards(MyAPIKeyQuery(), )
async def get_guarded_items(self):
return {'message': 'worked fine with `key`=`supersecret`'}
UseGuards
decorator, like the version
decorator, takes a list of values as an argument. During a request, the provided guards are called in the order in which they are provided. This allows you to apply multiple guards to a single route function and have them executed in a specific order. This is useful for applying multiple levels of security or access control to a single endpoint.
Each guard class has a can_execute
function that is called in the order specified by the decorator, if any of the guard's can_execute
function returns False, the route function will not be executed.
Command Decorators¶
The command
decorator is used to convert a decorated function into a command that can be executed through the Ellar command-line interface (CLI) actions. This allows you to define custom commands that can be run from the command-line, which can be useful for tasks such as running database migrations, generating code, or other tasks that can be automated.
Module Function Decorators¶
-
@exception_handler
: This decorator is used to register a function as an exception handler. This function will be called when an unhandled exception occurs during a request. It should take the exception instance as its only argument and return a response object. -
@middleware
: This decorator is used to register a function as middleware. Middlewares are called for each incoming request and can be used to modify the request or response, or perform any other actions before or after the request is handled. -
@template_filter
: This decorator is used to register a function as a Jinja2 template filter. -
@template_global
: This decorator is used to register a function as a global variable available in all Jinja2 templates. -
@template_context
: This decorator is used to register a function as a global template context for dynamic template context processing.
These decorators can be used to define functions that will be executed at specific points in the application's lifecycle. They provide a way to separate and organize the different parts of an application. See Module Additional Configuration for examples on how these decorator functions are used.