Skip to content

Commit d68358e

Browse files
authored
Add middleware (deriv-com#10)
* Added middleware support, and fixed some small problems * refactor MiddleWares * add doc * remove unused line
1 parent 165abcb commit d68358e

File tree

7 files changed

+163
-13
lines changed

7 files changed

+163
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.1.2
4+
5+
Added middleware support
6+
37
## 0.1.1
48

59
### Fixed:

deriv_api/deriv_api.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@
2020
from deriv_api.in_memory import InMemory
2121
from deriv_api.subscription_manager import SubscriptionManager
2222
from deriv_api.utils import is_valid_url
23+
from deriv_api.middlewares import MiddleWares
2324

2425
# TODO NEXT subscribe is not calling deriv_api_calls. that's , args not verified. can we improve it ?
25-
# TODO list these features missed
26-
# middleware is missed
2726

2827
logging.basicConfig(
2928
format="%(asctime)s %(message)s",
@@ -39,7 +38,7 @@
3938
'deriv_api.deriv_api.DerivAPI.disconnect': False,
4039
'deriv_api.deriv_api.DerivAPI.send': False,
4140
'deriv_api.deriv_api.DerivAPI.wsconnection': False,
42-
'deriv_api.deriv_api.DerivAPI.storage': False
41+
'deriv_api.deriv_api.DerivAPI.storage': False,
4342
}
4443

4544

@@ -70,10 +69,13 @@ class DerivAPI(DerivAPICalls):
7069
Language of the API communication
7170
brand : String
7271
Brand name
73-
middleware : String
74-
A middleware to call on certain API actions
72+
middleware : MiddleWares
73+
middlewares to call on certain API actions. Now two middlewares are supported: sendWillBeCalled and
74+
sendIsCalled
7575
Properties
7676
----------
77+
cache: Cache
78+
Temporary cache default to InMemory
7779
storage : Cache
7880
If specified, uses a more persistent cache (local storage, etc.)
7981
events: Observable
@@ -88,6 +90,7 @@ def __init__(self, **options: str) -> None:
8890
brand = options.get('brand', '')
8991
cache = options.get('cache', InMemory())
9092
storage: any = options.get('storage')
93+
self.middlewares: MiddleWares = options.get('middlewares', MiddleWares())
9194
self.wsconnection: Optional[WebSocketClientProtocol] = None
9295
self.wsconnection_from_inside = True
9396
self.shouldReconnect = False
@@ -251,15 +254,23 @@ async def send(self, request: dict) -> dict:
251254
API response
252255
"""
253256

257+
send_will_be_called = self.middlewares.call('sendWillBeCalled', {'request': request})
258+
if send_will_be_called:
259+
return send_will_be_called
260+
254261
self.events.on_next({'name': 'send', 'data': request})
255262
response_future = self.send_and_get_source(request).pipe(op.first(), op.to_future())
256263

257264
response = await response_future
258265
self.cache.set(request, response)
259266
if self.storage:
260267
self.storage.set(request, response)
268+
send_is_called = self.middlewares.call('sendIsCalled', {'response': response, 'request': request})
269+
if send_is_called:
270+
return send_is_called
261271
return response
262272

273+
263274
def send_and_get_source(self, request: dict) -> Subject:
264275
"""
265276
Send message and returns Subject

deriv_api/middlewares.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import Callable, Union
2+
3+
_implemented_middlewares = ['sendWillBeCalled', 'sendIsCalled']
4+
5+
6+
class MiddleWares:
7+
"""
8+
A class that help to manage middlewares
9+
10+
Examples:
11+
middlewares = MiddleWares()
12+
middlewares.add('sendWillBeCalled', lanmbda req: print(req))
13+
middlewares = Middlewares({'sendWillBeCalled': lambda req: print(req)})
14+
middleware->call('sendWillBeCalled', arg1, arg2)
15+
16+
Parameters:
17+
options:
18+
dict with following key value pairs
19+
key: string, middleware name
20+
value: function, middleware code
21+
"""
22+
23+
def __init__(self, middlewares: dict = {}):
24+
self.middlewares = {}
25+
for name in middlewares.keys():
26+
self.add(name, middlewares[name])
27+
28+
def add(self, name: str, code: Callable[..., bool]) -> None:
29+
"""
30+
Add middleware
31+
32+
Parameters:
33+
name: Str
34+
middleware name
35+
code: function
36+
middleware code
37+
"""
38+
if not isinstance(name, str):
39+
raise Exception(f"name {name} should be a string")
40+
if not isinstance(code, Callable):
41+
raise Exception(f"code {code} should be a Callable object")
42+
if name in _implemented_middlewares:
43+
self.middlewares[name] = code
44+
else:
45+
raise Exception(f"{name} is not supported in middleware")
46+
47+
def call(self, name: str, args: dict) -> Union[None, dict]:
48+
"""
49+
Call middleware and return the result if there is such middleware
50+
51+
Parameters
52+
----------
53+
name: string
54+
args: list
55+
the args that will feed to middleware
56+
57+
Returns
58+
-------
59+
If there is such middleware, then return the result of middleware
60+
else return None
61+
"""
62+
63+
if name not in self.middlewares:
64+
return None
65+
return self.middlewares[name](args)

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[pytest]
22
testpaths = tests
3+
asyncio_mode=auto

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
setup(
44
name='python_deriv_api',
55
packages=find_packages(include=['deriv_api']),
6-
version='0.1.1',
6+
version='0.1.2',
77
description='Python bindings for deriv.com websocket API',
88
author='Deriv Group Services Ltd',
99
author_email='learning+python@deriv.com',

tests/test_deriv_api.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import asyncio
2-
import sys
3-
import traceback
42
import pytest
53
import pytest_mock
64
import reactivex
@@ -15,6 +13,9 @@
1513
import json
1614
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
1715

16+
from deriv_api.middlewares import MiddleWares
17+
18+
1819
class MockedWs:
1920
def __init__(self):
2021
self.data = []
@@ -154,6 +155,55 @@ async def test_simple_send():
154155
wsconnection.clear()
155156
await api.clear()
156157

158+
@pytest.mark.asyncio
159+
async def test_middleware():
160+
wsconnection = MockedWs()
161+
send_will_be_called_args = None
162+
send_is_called_args = None
163+
send_will_be_called_return = None
164+
send_is_called_return = None
165+
def send_will_be_called(args):
166+
nonlocal send_will_be_called_args
167+
send_will_be_called_args = args
168+
nonlocal send_will_be_called_return
169+
return send_will_be_called_return
170+
171+
def send_is_called(args):
172+
nonlocal send_is_called_args
173+
send_is_called_args = args
174+
nonlocal send_is_called_return
175+
return send_is_called_return
176+
177+
api = deriv_api.DerivAPI(connection = wsconnection, middlewares = MiddleWares({'sendWillBeCalled': send_will_be_called, 'sendIsCalled': send_is_called}))
178+
req1 = {"ping": 2}
179+
180+
# test sendWillBeCalled return true value, sendIsCalled will be not called and the request will not be sent to ws
181+
data1 = {"echo_req":req1,"msg_type": "ping", "pong": 1,}
182+
wsconnection.add_data(data1)
183+
send_will_be_called_return = {'value': 1} # middleware sendWillBeCalled will return a dict
184+
response = await api.send(req1)
185+
assert response == send_will_be_called_return # api will return the value of sendWillBeCalled returned
186+
assert send_will_be_called_args == {'request': req1}
187+
assert len(wsconnection.called['send']) == 0 # ws send is not called
188+
189+
# test sendWillBeCalled return false value, ws send will be called and sendIsCalled will be called
190+
send_will_be_called_return = None
191+
send_is_called_return = None # middleware sendIsCalled will return false
192+
response = await api.send(req1)
193+
expected_response = data1.copy()
194+
expected_response['req_id'] = 1
195+
assert response == expected_response # response is the value that ws returned
196+
assert send_is_called_args == {'response': expected_response, 'request': req1}
197+
198+
# test sendWillBeCalled return false , and sendIsCalled return a true value
199+
send_is_called_return = {'value': 2}
200+
wsconnection.add_data(data1)
201+
response = await api.send(req1)
202+
assert response == {'value': 2} # will get what sendIsCalled return
203+
204+
wsconnection.clear()
205+
await api.clear()
206+
157207
@pytest.mark.asyncio
158208
async def test_subscription():
159209
wsconnection = MockedWs()
@@ -383,18 +433,13 @@ async def recv(self):
383433
api.wsconnection_from_inside = True
384434
last_error = api.sanity_errors.pipe(op.first(), op.to_future())
385435
await asyncio.sleep(0.1) # waiting for init finished
386-
print("here 382")
387436
await api.disconnect() # it will set connected as 'Closed by disconnect', and cause MockedWs2 raising `test disconnect`
388-
print("here 384")
389437
assert isinstance((await last_error), ConnectionClosedOK), 'sanity error get errors'
390-
print("here 386")
391438
with pytest.raises(ConnectionClosedOK, match='Closed by disconnect'):
392439
await api.send({'ping': 1}) # send will get same error
393-
print("here 389")
394440
with pytest.raises(ConnectionClosedOK, match='Closed by disconnect'):
395441
await api.connected # send will get same error
396442
wsconnection.clear()
397-
print("here 391")
398443
await api.clear()
399444

400445
# closed by remote

tests/test_middlewares.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import pytest
2+
import re
3+
from deriv_api.middlewares import MiddleWares
4+
5+
def test_middlewares():
6+
middlewares = MiddleWares()
7+
isinstance(middlewares, MiddleWares)
8+
9+
with pytest.raises(Exception, match = r"should be a string") as err:
10+
MiddleWares({123: lambda i: i+1})
11+
12+
with pytest.raises(Exception, match = r"should be a Callable") as err:
13+
MiddleWares({"name": 123})
14+
15+
with pytest.raises(Exception, match = r"not supported"):
16+
MiddleWares({"hello": lambda i: i})
17+
18+
call_args = []
19+
def send_will_be_called(*args):
20+
nonlocal call_args
21+
call_args = args
22+
middlewares.add('sendWillBeCalled', send_will_be_called)
23+
middlewares.call('sendWillBeCalled', 123)
24+
assert call_args == (123,)

0 commit comments

Comments
 (0)