Merge pull request #30 from Irishery/master

Added rate limits for all endpoints
api-breakage
Morty Space 4 years ago committed by GitHub
commit bb630e4be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      README.md
  2. 3
      setup.py
  3. 2
      src/cryptocom/exchange/__init__.py
  4. 59
      src/cryptocom/exchange/api.py
  5. 43
      tests/test_api.py

@ -27,6 +27,7 @@ Exchange original API docs: [https://exchange-docs.crypto.com](https://exchange-
### Changelog
- **0.9.3** - added RPS limiter by @Irishery
- **0.9.2** - fixed event loop import level
- **0.9.1** - fixed Windows bug with asyncio event loop
- **0.9.0** - updated coins, refactored wallet transactions

@ -38,7 +38,8 @@ setup(
packages=find_packages('src'),
install_requires=[
'aiohttp',
'cached-property'
'cached-property',
'aiolimiter'
],
extras_require={
'dev': [

@ -23,4 +23,4 @@ __all__ = [
'ApiError', 'ApiProvider'
]
__version__ = '0.9.2'
__version__ = '0.9.3'

@ -7,12 +7,41 @@ import asyncio
import hashlib
from urllib.parse import urljoin
from aiolimiter import AsyncLimiter
import aiohttp
from aiohttp.client_exceptions import ContentTypeError
RATE_LIMITS = {
# order methods
(
'private/create-order',
'private/cancel-order',
'private/cancel-all-orders',
'private/margin/create-order',
'private/margin/cancel-order',
'private/margin/cancel-all-orders',
): (14, 0.1),
# order detail methods
(
'private/get-order-detail',
'private/margin/get-order-detail',
): (29, 0.1),
# general trade methods
(
'private/get-trades',
'private/margin/get-trades',
'private/get-order-history',
'private/margin/get-order-history'
): (1, 1)
}
class ApiError(Exception):
pass
@ -24,16 +53,24 @@ class ApiProvider:
auth_required=True, timeout=25, retries=6,
root_url='https://api.crypto.com/v2/',
ws_root_url='wss://stream.crypto.com/v2/', logger=None):
self.api_key = api_key
self.api_secret = api_secret
self.root_url = root_url
self.ws_root_url = ws_root_url
self.timeout = timeout
self.retries = retries
self.last_request_path = ''
self.rate_limiters = {}
# NOTE: do not change this, due to crypto.com rate-limits
# TODO: add more strict settings, req/per second or milliseconds
self.semaphore = asyncio.Semaphore(20)
for urls in RATE_LIMITS:
for url in urls:
self.rate_limiters[url] = AsyncLimiter(*RATE_LIMITS[urls])
# limits for not matched methods
self.general_private_limit = AsyncLimiter(3, 0.1)
self.general_public_limit = AsyncLimiter(100, 1)
if not auth_required:
return
@ -76,15 +113,29 @@ class ApiProvider:
).hexdigest()
return data
def get_limit(self, path):
if path in self.rate_limiters.keys():
return self.rate_limiters[path]
else:
if path.startswith('private'):
return self.general_private_limit
elif path.startswith('public'):
return self.general_public_limit
else:
raise ApiError(f'Wrong path: {path}')
async def request(self, method, path, params=None, data=None, sign=False):
original_data = data
timeout = aiohttp.ClientTimeout(total=self.timeout)
limiter = self.get_limit(path)
for count in range(self.retries + 1):
if sign:
data = self._sign(path, original_data)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with self.semaphore:
async with limiter:
resp = await session.request(
method, urljoin(self.root_url, path),
params=params, json=data,

@ -1,3 +1,4 @@
import asyncio
import os
import time
import pytest
@ -55,3 +56,45 @@ async def test_wrong_api_response():
api = cro.ApiProvider(auth_required=False)
with pytest.raises(cro.ApiError):
await api.post('account')
@pytest.mark.asyncio
async def test_api_rate_limits():
api = cro.ApiProvider(from_env=True)
pair = cro.pairs.CRO_USDT
page = 0
page_size = 50
params = {'page_size': page_size, 'page': page}
if pair:
params['instrument_name'] = pair.name
start_time = time.time()
tasks = [api.post('private/get-order-history', {'params': params}) for i in range(2)]
await asyncio.gather(*tasks)
finish_time = (time.time() - start_time)
assert finish_time > 1
start_time = time.time()
tasks = [api.post('private/get-order-history', {'params': params}) for _ in range(5)]
await asyncio.gather(*tasks)
finish_time = time.time() - start_time
assert finish_time > 4
start_time = time.time()
tasks = [api.get('public/get-instruments') for _ in range(200)]
await asyncio.gather(*tasks)
finish_time = time.time() - start_time
assert finish_time > 1
start_time = time.time()
tasks = [api.post('private/get-order-history', {'params': params}) for _ in range(4)]
await asyncio.gather(*tasks)
finish_time = time.time() - start_time
assert finish_time > 3

Loading…
Cancel
Save