Extension Model#
The BDK Extension Mechanism is still an experimental feature, contracts might be subject to breaking changes in following versions.
Overview#
The Extension API is available through the module symphony.bdk.core.extension
but other modules might be required
depending on what your extension needs to use.
Registering Extensions#
Extensions are registered programmatically via the ExtensionService
:
async with SymphonyBdk(config) as bdk:
extension_service = bdk.extensions()
extension_service.register(MyExtensionType)
Service Provider Extension#
A Service Provider extension is a specific type of extension loaded on demand when calling the
ExtensionService#service(MyExtensionType)
method.
To make your extension Service Provider, your extension definition must implement the method get_service(self)
:
# The Service implementation class.
class MyBdkExtensionService:
def say_hello(self, name):
print(f"Hello, {name}!")
# The Extension definition class.
class MyBdkExtension:
def __init__(self):
self._service = MyBdkExtensionService()
def get_service(self):
return self._service
# Usage example.
async def run():
config = BdkConfigLoader.load_from_symphony_dir("config.yaml")
async with SymphonyBdk(config) as bdk:
extension_service = bdk.extensions()
extension_service.register(MyBdkExtensionService)
service = extension_service.service(MyBdkExtensionService)
service.say_hello("Symphony")
asyncio.run(run())
BDK Aware Extensions#
The BDK Extension Model allows extensions to access to some core objects such as the configuration or the api clients.
Developers that wish to use these objects are free to implement a set of abstract base classes all suffixed with
the Aware
keyword.
If an extension do not extend one of Aware
classes but implements the corresponding method, the latter will be used as
the ExtensionService
uses duck typing internally.
BdkConfigAware
#
The abc symphony.bdk.core.extension.BdkConfigAware
allows extensions to read the BdkConfig
:
class MyBdkExtension(BdkConfigAware):
def __init__(self):
self._config = None
def set_config(self, config):
self._config = config
BdkApiClientFactoryAware
#
The abc symphony.bdk.core.extension.BdkApiClientFactoryAware
can be used by extensions that need to use
the ApiClientFactory
:
class MyBdkExtension(BdkApiClientFactoryAware):
def __init__(self):
self._api_client_factory = None
def set_api_client_factory(self, api_client_factory):
self._api_client_factory = api_client_factory
BdkAuthenticationAware
#
The abc symphony.bdk.core.extension.BdkAuthenticationAware
can be used by extensions that need to rely on the service
account authentication session (AuthSession
), which provides the sessionToken
and
keyManagerToken
that are used to call the Symphony’s APIs:
class MyBdkExtension(BdkAuthenticationAware):
def __init__(self):
self._auth_session = None
def set_auth_session(self, auth_session):
self._auth_session = auth_session
Retry#
In order to leverage the retry mechanism your service class should have the field self._retry_config
of type
BdkRetryConfig
and each function that needs a retry mechanism can use the @retry
decorator. This decorator will
reuse the config declared in self._retry_config
.
The default retry mechanism is defined here: refresh_session_if_unauthorized
.
It retries on connection errors (more precisely ClientConnectionError
and TimeoutError
) and
on the following HTTP status codes:
401
429
codes greater than or equal to 500.
In case of unauthorized, it will call await self._auth_session.refresh()
before retrying.
Following is a sample code to show how it can be used:
from symphony.bdk.core.extension import BdkExtensionServiceProvider, BdkAuthenticationAware, BdkConfigAware
from symphony.bdk.core.retry import retry
class MyExtension(BdkConfigAware, BdkAuthenticationAware, BdkExtensionServiceProvider):
def __init__(self):
self._config = None
self._auth_session = None
def set_config(self, config):
self._config = config
def set_bot_session(self, auth_session):
self._auth_session = auth_session
def get_service(self):
return MyService(self._config.retry, self._auth_session)
class MyService:
def __init__(self, retry_config, auth_session):
self._retry_config = retry_config # used by the @retry decorator
self._auth_session = auth_session # default retry logic will call refresh on self._auth_session
@retry
async def my_service_method(self):
pass # do stuff which will be retried
Retry conditions and mechanism can be customized as follows:
async def my_retry_mechanism(retry_state):
"""Function used by the retry decorator to check if a function call has to be retried.
:param retry_state: current retry state, of type RetryCallState: https://tenacity.readthedocs.io/en/latest/#retrycallstate
:return: True if we want to retry, False otherwise
"""
if retry_state.outcome.failed:
exception = retry_state.outcome.exception() # exception that lead to the failure
if condition_on_exception(exception):
# do stuff to recover the exception
# method args can be accessed as follows: retry_state.args
return True # return True to retry the function
return False # return False to not retry and make function call fail
class MyService:
def __init__(self, retry_config, auth_session):
self._retry_config = retry_config # used by the @retry decorator
self._auth_session = auth_session # default retry logic will call refresh on self._auth_session
@retry(retry=my_retry_mechanism)
async def my_service_method(self):
pass # do stuff which will be retried