Recipes
This is a collection of how to use certain features of Antidote or simply examples of what can be done.
Use interfaces
Antidote supports the distinction interface/implementation out of the box with the
function decorator implementation()
. The result of the function will be the
retrieved dependency for the specified interface. Typically this means a class registered
as a service or one that can be provided by a factory.
from antidote import implementation, service, inject, Constants, const
class Database:
pass
@service
class PostgresDB(Database):
pass
@service
class MySQLDB(Database):
pass
class Conf(Constants):
DB_CONN_STR = const[str]('postgres:...')
# permanent is True by default. If you want to choose on each call which implementation
# should be used, set it to False.
@implementation(Database, permanent=True)
def local_db(db_conn_str: str = Conf.DB_CONN_STR) -> object:
db, *rest = db_conn_str.split(':')
if db == 'postgres':
# Complex dependencies are supported
return PostgresDB
elif db == 'mysql':
# But you can also simply return the class
return MySQLDB
else:
raise RuntimeError(f"{db} is not a supported database")
Similar to a factory()
you must specify the source of the dependency:
>>> @inject
... def f(db: Database = inject.me(source=local_db)):
... return db
>>> f()
<PostgresDB ...>
>>> from antidote import world
>>> world.get(Database, source=local_db)
<PostgresDB ...>
Lazily call a function
Calling lazily a function can be done with lazy
or
LazyMethodCall
for methods. Both will pass any arguments passed on
and can either be singletons or not.
Function
import requests
from antidote import LazyCall, inject
def fetch_remote_conf(name):
return requests.get(f"https://example.com/conf/{name}")
CONF_A = LazyCall(fetch_remote_conf)("conf_a")
@inject(dependencies=(CONF_A,))
def f(conf):
return conf
Using CONF_A
as a representation of the result allows one to easily identify
where this dependency is needed. Moreover neither f
nor its caller needs to
be aware on how to call fetch_remote_conf
.
Method
Lazily calling a method requires the class to be Service
.
from antidote import LazyMethodCall, service
@service
class ExampleCom:
def get(url):
return requests.get(f"https://example.com{url}")
STATUS = LazyMethodCall(get, singleton=False)("/status")
Note
If you intend to define lazy constants, consider using
Constants
instead.
Create a stateful factory
Antidote supports stateful factories simply by using defining a class as a factory:
from antidote import factory
class ID:
def __init__(self, id: str):
self.id = id
def __repr__(self):
return "ID(id='{}')".format(self.id)
@factory(singleton=False)
class IDFactory:
def __init__(self, id_prefix: str = "example"):
self._prefix = id_prefix
self._next = 1
def __call__(self) -> ID:
id = ID("{}_{}".format(self._prefix, self._next))
self._next += 1
return id
>>> from antidote import world
>>> world.get(ID, source=IDFactory)
ID(id='example_1')
>>> world.get(ID, source=IDFactory)
ID(id='example_2')
In this example we choose to inject id_prefix
in the __init__()
, but we
also could have done it in the __call__()
. Both are injected by default, but they
have different use cases. The factory itself is always a singleton, so static dependencies
should be injected through __init__()
. If you need dependencies that changes, get
them through __call__()
. Obviously you can change that behavior through the
Factory.Conf
: defined in __antidote__
.
Note
Stateful factories can also be used to provide dependencies that have a more complex
scope than Antidote provides (singleton or not). Although, if you need to handle some
scope for multiples dependencies it might be worth just extending Antidote through a
Provider
.
Configuration
Here are some examples on how to use Constants
to handle configuration coming
from different sources.
From the environment
import os
from typing import Optional
from antidote import Constants, const
class Env(Constants):
SECRET = const[str]()
def provide_const(self, name: str, arg: Optional[object]):
return os.environ[name]
>>> from antidote import world, inject
>>> os.environ['SECRET'] = 'my_secret'
>>> world.get[str](Env.SECRET)
'my_secret'
>>> @inject
... def f(secret: str = Env.SECRET) -> str:
... return secret
>>> f()
'my_secret'
From a dictionary
Configuration can be stored in a lot of different formats, or even be retrieved on a remote endpoint at start-up. Most of the time you would be able to easily convert it to a dictionary and use the following:
import os
from typing import Optional
from antidote import Constants, const
class Conf(Constants):
HOST = const[str]('host')
AWS_API_KEY = const[str]('aws.api_key')
def __init__(self):
# Load your configuration into a dictionary
self._raw_conf = {
"host": "localhost",
"aws": {
"api_key": "my key"
}
}
def provide_const(self, name: str, arg: Optional[str]):
from functools import reduce
assert arg is not None and isinstance(arg, str) # sanity check
return reduce(dict.get, arg.split('.'), self._raw_conf) # type: ignore
>>> from antidote import world, inject
>>> world.get[str](Conf.HOST)
'localhost'
>>> world.get(Conf.AWS_API_KEY)
'my key'
>>> @inject
... def f(key: str = Conf.AWS_API_KEY) -> str:
... return key
>>> f()
'my key'
Specifying a type / Using Enums
You can specify a type when using const()
. It’s main purpose is to provide
a type for Mypy when the constants are directly accessed from an instance. However
Constants
will also automatically force the cast if the type is one
of str
, float
or int
. You can control this behavior with
the auto_cast
argument of Conf
. A typical use case
would be to support enums as presented here:
from enum import Enum
from typing import Optional
from antidote import Constants, const
class Env(Enum):
PROD = 'prod'
PREPRDO = 'preprod'
class Conf(Constants):
__antidote__ = Constants.Conf(auto_cast=[int, Env])
DB_PORT = const[int]()
ENV = const[Env]()
def provide_const(self, name: str, arg: Optional[object]):
return {'db_port': '5432', 'env': 'prod'}[name.lower()]
>>> from antidote import world, inject
>>> Conf().DB_PORT
5432
>>> Conf().ENV
<Env.PROD: 'prod'>
>>> world.get[int](Conf.DB_PORT)
5432
>>> world.get[Env](Conf.ENV)
<Env.PROD: 'prod'>
>>> @inject
... def f(env: Env = Conf.ENV) -> Env:
... return env
>>> f()
<Env.PROD: 'prod'>
The goal of this is to simplify common operations when manipulating the environment
or configuration files. If you need complex behavior, consider using a service for this
or define your Configuration class as public=True
in Conf
and use it as a one.
Default values
Default values can be specified in const()
:
import os
from antidote import Constants, const
class Env(Constants):
HOST = const[str]('HOST', default='localhost')
def get(self, value):
return os.environ[value]
It will be use if get
raises a py:exec:KeyError. For more complex behavior,
using a collections.ChainMap
which loads your defaults and the user is a good
alternative:
from collections import ChainMap
from antidote import Constants, const
class Configuration(Constants):
def __init__(self):
user_conf = dict() # load conf from a file, etc..
default_conf = dict()
# User conf will override default_conf
self._raw_conf = ChainMap(user_conf, default_conf)
An alternative to this would be using a configuration format that supports overrides, such as HOCON.
Scopes
A dependency may be associated with a scope. If so it’ll cached for as along as the scope is
valid. The most common scope being the singleton scope where dependencies are cached forever.
When the scope is set to None
, the dependency value will be retrieved each time.
Scopes can be create through world.scopes.new()
. The name is only used to
have a friendly identifier when debugging.
>>> from antidote import world
>>> REQUEST_SCOPE = world.scopes.new(name='request')
To use the newly created scope, use scope
parameters:
>>> from antidote import service
>>> @service(scope=REQUEST_SCOPE)
... class Dummy:
... pass
As Dummy
has been defined with a custom scope, the dependency value will
be kep as long as REQUEST_SCOPE
stays valid. That is to say, until you reset
it with world.scopes.reset()
:
>>> current = world.get(Dummy)
>>> current is world.get(Dummy)
True
>>> world.scopes.reset(REQUEST_SCOPE)
>>> current is world.get(Dummy)
False
In a Flask app for example you would then just reset the scope after each request:
from flask import Flask
app = Flask(__name__)
@app.after_request
def reset_request_scope():
world.scopes.reset(REQUEST_SCOPE)