Skip to content

Commit 2d5b82a

Browse files
Extension mechanism (#268)
* The Extension API is available through the module symphony.bdk.core.extension. Extensions are registered programmatically via the ExtensionService. 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. * As part of this PR we also leveraged this new Extension Mechanism to implement a new custom extension to support the Symphony Groups APIs.
1 parent 24bfc42 commit 2d5b82a

19 files changed

+1243
-116
lines changed

docsrc/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Contents
1313
* [Application service](markdown/application_service.md)
1414
* [Signal service](markdown/signal_service.md)
1515
* [Presence service](markdown/presence_service.md)
16+
* [Extending the BDK](markdown/extension.md)
1617
* [FAQ](markdown/faq.md)
1718

1819
```eval_rst
@@ -47,6 +48,7 @@ The reference documentation consists of the following sections:
4748
* [Connection service](markdown/connection_service.md): Managing connections between users
4849
* [Signal service](markdown/signal_service.md): Managing signals, subscribing/unsubscribing to signals
4950
* [Presence service](markdown/presence_service.md): Reacting and managing presences
51+
* [Extending the BDK](markdown/extension.md): How to use or develop BDK extensions
5052

5153
### Technical Documentation
5254
* Information on how we generate client side code from swagger specs in the

docsrc/markdown/extension.md

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Extension Model
2+
3+
> The BDK Extension Mechanism is still an experimental feature, contracts might be subject to **breaking changes**
4+
> in following versions.
5+
6+
## Overview
7+
8+
The Extension API is available through the module `symphony.bdk.core.extension` but other modules might be required
9+
depending on what your extension needs to use.
10+
11+
## Registering Extensions
12+
13+
Extensions are registered _programmatically_ via the `ExtensionService`:
14+
15+
```python
16+
async with SymphonyBdk(config) as bdk:
17+
extension_service = bdk.extensions()
18+
extension_service.register(MyExtensionType)
19+
```
20+
21+
## Service Provider Extension
22+
23+
A _Service Provider_ extension is a specific type of extension loaded on demand when calling the
24+
`ExtensionService#service(MyExtensionType)` method.
25+
26+
To make your extension _Service Provider_, your extension definition must implement the method `get_service(self)`:
27+
28+
```python
29+
# The Service implementation class.
30+
class MyBdkExtensionService:
31+
def say_hello(self, name):
32+
print(f"Hello, {name}!")
33+
34+
35+
# The Extension definition class.
36+
class MyBdkExtension:
37+
def __init__(self):
38+
self._service = MyBdkExtensionService()
39+
40+
def get_service(self):
41+
return self._service
42+
43+
44+
# Usage example.
45+
async def run():
46+
config = BdkConfigLoader.load_from_symphony_dir("config.yaml")
47+
48+
async with SymphonyBdk(config) as bdk:
49+
extension_service = bdk.extensions()
50+
extension_service.register(MyBdkExtensionService)
51+
52+
service = extension_service.service(MyBdkExtensionService)
53+
service.say_hello("Symphony")
54+
55+
56+
asyncio.run(run())
57+
```
58+
59+
## BDK Aware Extensions
60+
61+
The BDK Extension Model allows extensions to access to some core objects such as the configuration or the api clients.
62+
Developers that wish to use these objects are free to implement a set of abstract base classes all suffixed with
63+
the `Aware` keyword.
64+
65+
If an extension do not extend one of `Aware` classes but implements the corresponding method, the latter will be used as
66+
the `ExtensionService` uses duck typing internally.
67+
68+
### `BdkConfigAware`
69+
70+
The abc `symphony.bdk.core.extension.BdkConfigAware` allows extensions to read the `BdkConfig`:
71+
72+
```python
73+
class MyBdkExtension(BdkConfigAware):
74+
def __init__(self):
75+
self._config = None
76+
77+
def set_config(self, config):
78+
self._config = config
79+
```
80+
81+
### `BdkApiClientFactoryAware`
82+
83+
The abc `symphony.bdk.core.extension.BdkApiClientFactoryAware` can be used by extensions that need to use
84+
the `ApiClientFactory`:
85+
86+
```python
87+
class MyBdkExtension(BdkApiClientFactoryAware):
88+
def __init__(self):
89+
self._api_client_factory = None
90+
91+
def set_api_client_factory(self, api_client_factory):
92+
self._api_client_factory = api_client_factory
93+
```
94+
95+
### `BdkAuthenticationAware`
96+
97+
The abc `symphony.bdk.core.extension.BdkAuthenticationAware` can be used by extensions that need to rely on the service
98+
account authentication session (`AuthSession`), which provides the `sessionToken` and
99+
`keyManagerToken` that are used to call the Symphony's APIs:
100+
101+
```python
102+
class MyBdkExtension(BdkAuthenticationAware):
103+
def __init__(self):
104+
self._auth_session = None
105+
106+
def set_auth_session(self, auth_session):
107+
self._auth_session = auth_session
108+
```
109+
110+
## Retry
111+
112+
In order to leverage the retry mechanism your service class should have the field `self._retry_config` of type
113+
`BdkRetryConfig` and each function that needs a retry mechanism can use the `@retry` decorator. This decorator will
114+
reuse the config declared in `self._retry_config`.
115+
116+
The default retry mechanism is defined here: [`refresh_session_if_unauthorized`](../_autosummary/symphony.bdk.core.retry.strategy.refresh_session_if_unauthorized.html).
117+
It retries on connection errors (more precisely `ClientConnectionError` and `TimeoutError`) and
118+
on the following HTTP status codes:
119+
* 401
120+
* 429
121+
* codes greater than or equal to 500.
122+
123+
In case of unauthorized, it will call `await self._auth_session.refresh()` before retrying.
124+
125+
Following is a sample code to show how it can be used:
126+
```python
127+
from symphony.bdk.core.extension import BdkExtensionServiceProvider, BdkAuthenticationAware, BdkConfigAware
128+
from symphony.bdk.core.retry import retry
129+
130+
class MyExtension(BdkConfigAware, BdkAuthenticationAware, BdkExtensionServiceProvider):
131+
def __init__(self):
132+
self._config = None
133+
self._auth_session = None
134+
135+
def set_config(self, config):
136+
self._config = config
137+
138+
def set_bot_session(self, auth_session):
139+
self._auth_session = auth_session
140+
141+
def get_service(self):
142+
return MyService(self._config.retry, self._auth_session)
143+
144+
145+
class MyService:
146+
def __init__(self, retry_config, auth_session):
147+
self._retry_config = retry_config # used by the @retry decorator
148+
self._auth_session = auth_session # default retry logic will call refresh on self._auth_session
149+
150+
@retry
151+
async def my_service_method(self):
152+
pass # do stuff which will be retried
153+
```
154+
155+
Retry conditions and mechanism can be customized as follows:
156+
```python
157+
async def my_retry_mechanism(retry_state):
158+
"""Function used by the retry decorator to check if a function call has to be retried.
159+
160+
:param retry_state: current retry state, of type RetryCallState: https://tenacity.readthedocs.io/en/latest/#retrycallstate
161+
:return: True if we want to retry, False otherwise
162+
"""
163+
if retry_state.outcome.failed:
164+
exception = retry_state.outcome.exception() # exception that lead to the failure
165+
if condition_on_exception(exception):
166+
# do stuff to recover the exception
167+
# method args can be accessed as follows: retry_state.args
168+
return True # return True to retry the function
169+
return False # return False to not retry and make function call fail
170+
171+
class MyService:
172+
def __init__(self, retry_config, auth_session):
173+
self._retry_config = retry_config # used by the @retry decorator
174+
self._auth_session = auth_session # default retry logic will call refresh on self._auth_session
175+
176+
@retry(retry=my_retry_mechanism)
177+
async def my_service_method(self):
178+
pass # do stuff which will be retried
179+
```
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import asyncio
2+
import logging.config
3+
from pathlib import Path
4+
5+
from symphony.bdk.core.config.loader import BdkConfigLoader
6+
from symphony.bdk.core.symphony_bdk import SymphonyBdk
7+
from symphony.bdk.ext.group import SymphonyGroupBdkExtension, SymphonyGroupService
8+
from symphony.bdk.gen.group_model.base_profile import BaseProfile
9+
from symphony.bdk.gen.group_model.create_group import CreateGroup
10+
from symphony.bdk.gen.group_model.member import Member
11+
from symphony.bdk.gen.group_model.owner import Owner
12+
from symphony.bdk.gen.group_model.status import Status
13+
from symphony.bdk.gen.group_model.update_group import UpdateGroup
14+
15+
logging.config.fileConfig(Path(__file__).parent.parent / "logging.conf", disable_existing_loggers=False)
16+
17+
18+
async def run():
19+
async with SymphonyBdk(BdkConfigLoader.load_from_symphony_dir("config.yaml")) as bdk:
20+
bdk.extensions().register(SymphonyGroupBdkExtension)
21+
group_service: SymphonyGroupService = bdk.extensions().service(SymphonyGroupBdkExtension)
22+
23+
# list groups
24+
groups = await group_service.list_groups(status=Status("ACTIVE"))
25+
logging.debug(f"List groups: {groups}")
26+
27+
# create a new group
28+
profile = BaseProfile(display_name="Mary's SDL")
29+
member = Member(member_tenant=190, member_id=13056700580915)
30+
create_group = CreateGroup(type="SDL", owner_type=Owner(value="TENANT"), owner_id=190, name="Another SDL",
31+
members=[member], profile=profile)
32+
group = await group_service.insert_group(create_group=create_group)
33+
logging.debug(f"Group created: {group}")
34+
35+
# update group name
36+
update_group = UpdateGroup(name="Updated name", type=group.type, owner_type=Owner(value="TENANT"),
37+
owner_id=group.owner_id, id=group.id, e_tag=group.e_tag,
38+
status=Status(value="ACTIVE"), profile=profile, members=[member])
39+
group = await group_service.update_group(if_match=group.e_tag, group_id=group.id, update_group=update_group)
40+
logging.debug(f"Group after name update: {group}")
41+
42+
# add member to a group
43+
group = await group_service.add_member_to_group(group.id, 13056700580913)
44+
logging.debug(f"Group after a new member is added: {group}")
45+
46+
# update group avatar
47+
image_base_64 = "base_64_format_image"
48+
group = await group_service.update_avatar(group_id=group.id, image=image_base_64)
49+
logging.debug(f"Group after avatar update: {group}")
50+
51+
# get a group by id
52+
group = await group_service.get_group(group_id=group.id)
53+
logging.debug(f"Retrieve group by id: {group}")
54+
55+
# Delete group
56+
update_group = UpdateGroup(name=group.name, type=group.type, owner_type=Owner(value="TENANT"),
57+
owner_id=group.owner_id, id=group.id, e_tag=group.e_tag,
58+
status=Status(value="DELETED"), profile=profile, members=[member])
59+
group = await group_service.update_group(if_match=group.e_tag, group_id=group.id, update_group=update_group)
60+
logging.debug(f"Group removed: {group}")
61+
62+
63+
asyncio.run(run())

0 commit comments

Comments
 (0)