Skip to content

Commit

Permalink
Tests + Rate Limit
Browse files Browse the repository at this point in the history
  • Loading branch information
END committed Jul 26, 2023
1 parent 972dbdc commit 0a035a8
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 25 deletions.
54 changes: 31 additions & 23 deletions gondi/api/client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import asyncio
import datetime as dt
import logging
import os

import gql
from eth_account.messages import encode_defunct
from gql.dsl import dsl_gql, DSLSchema, DSLMutation, DSLQuery
from gql.transport.aiohttp import AIOHTTPTransport
from gql.transport.exceptions import TransportServerError
from siwe import SiweMessage
from web3.auto import w3

Expand All @@ -15,6 +18,8 @@
DOMAIN = "localhost"
URI = f"http://{DOMAIN}"
STATEMENT = "Sign in with Ethereum to the app."
COOLING_OFF = dt.timedelta(seconds=60)
RATE_LIMIT_CODE = 429


def requires_login(fn):
Expand Down Expand Up @@ -43,10 +48,11 @@ def __init__(self, env: Environment | None = Environment.LOCAL):
self._version = config.client_version
self._chain_id = config.chain_id
self._bearer = None
self._no_request_until = None

@property
def schema(self) -> "DSLSchema":
return self._ds
def bearer(self) -> str:
return self._bearer

@property
def lending_schema(self) -> "DSLSchema":
Expand All @@ -73,13 +79,11 @@ async def check_bearer(self):
await self.login()

async def query(self, query: "DSLQuery") -> dict:
async with self._lending_client as session:
return await session.execute(dsl_gql(query))
return await self._query(query)

@requires_login
async def auth_query(self, query: "DSLQuery") -> dict:
async with self._lending_client as session:
return await session.execute(dsl_gql(query))
return await self._query(query)

def _update_headers(self, bearer: str):
self._client.transport.headers = {"Authorization": f"Bearer {bearer}"}
Expand All @@ -105,26 +109,30 @@ def _get_siwe_message(self, nonce: str) -> SiweMessage:
nonce=nonce,
).prepare_message()

async def _query(self, query: "DSLQuery") -> dict | None:
if (
self._no_request_until is not None
and dt.datetime.now() < self._no_request_until
):
logging.warning(
"Going to sleep. Need to wait until: %s", self._no_request_until
)
await asyncio.sleep(
(self._no_request_until - dt.datetime.now()).total_seconds()
)
try:
async with self._lending_client as session:
query = await session.execute(dsl_gql(query))
self._no_request_until = None
except TransportServerError as e:
if e.code == RATE_LIMIT_CODE:
self._no_request_until = dt.datetime.now() + COOLING_OFF
logging.warning("Cooling off. Rate limit exceeded.")
return query

@staticmethod
def _get_schema(schema: str) -> str:
base_dir = f"{os.path.dirname(os.path.realpath(__file__))}/../resources"
filename = f"{base_dir}/{schema}.graphql"
with open(filename) as f:
return f.read()


async def test():
from gondi.api.query_provider import QueryProvider

client = Client(Environment.MAIN)
offers_input = inputs.OfferInput(only_single_nft_offers=True)
provider = QueryProvider(client)
print(await client.query(provider.get_offers(offers_input)))


if __name__ == "__main__":
import asyncio

loop = asyncio.get_event_loop()

loop.run_until_complete(test())
6 changes: 6 additions & 0 deletions gondi/api/inputs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from collections.abc import Iterable
from enum import Enum
from typing import Any, Generic, TypeVar

from dataclasses import asdict, dataclass

T = TypeVar("T")
Iterables = tuple | list | set


class AsKwargsMixin:
Expand All @@ -26,6 +28,10 @@ def _parsed(self, value) -> Any | dict[str, Any]:
for k, v in value.items()
if v is not None
}
if isinstance(value, Iterables):
return [self._parsed(v) for v in value if v is not None]
if isinstance(value, Enum):
return value.value
return value

@staticmethod
Expand Down
4 changes: 2 additions & 2 deletions gondi/resources/config.main
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ LIQUIDATOR: '0x237e4421C742d843Fdd96D22294D338507e17091'
MULTI_SOURCE_LOAN: '0xCa5a494Ca20483e21ec1E41FE1D9461Da77595Bd'
RANGE_VALIDATOR: '0x18905fc7F3AaB462394F45B69308509a6b75573b'

API_URL: "https://api.floridastreet.xyz/graphql"
LENDING_API_URL: "https://api.floridastreet.xyz/lending/graphql"
API_URL: "https://api.gondi.xyz/graphql"
LENDING_API_URL: "https://api.gondi.xyz/lending/graphql"
BLOCKCHAIN: "ethereum"
CLIENT_VERSION: 1
CHAIN_ID: 1
22 changes: 22 additions & 0 deletions scripts/get_offers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from gondi.common_utils import Environment
from gondi.api import inputs
from gondi.api.client import Client


async def test():
from gondi.api.query_provider import QueryProvider

client = Client(Environment.MAIN)
offers_input = inputs.OfferInput(statuses=[inputs.OfferStatus.ACTIVE])
provider = QueryProvider(client)
for i in range(300):
print(await client.query(provider.get_offers(offers_input)))
print(i)


if __name__ == "__main__":
import asyncio

loop = asyncio.get_event_loop()

loop.run_until_complete(test())
Empty file added test/api/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions test/api/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import unittest
from unittest.mock import patch

from gql.dsl import DSLQuery

from gondi.api.client import Client
from test.mocks import MockedGraphqlClient, MockedSession


GraphqlClientPatch = patch("gql.Client", MockedGraphqlClient)


@GraphqlClientPatch
class TestClient(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self) -> None:
self._session = MockedSession()

async def test_login(self):
client = Client()
await client.login()
self.assertEquals(
client.bearer, self._session.return_values["signInWithEthereum"]
)

async def test_check_bearer(self):
client = Client()
await client.check_bearer()
self.assertEquals(
client.bearer, self._session.return_values["signInWithEthereum"]
)

async def test_query(self):
client = Client()
sample_query = self._sample_query(client.lending_schema)
no_queries = len(self._session.queries)
await client.query(sample_query)
self.assertEqual(no_queries + 1, len(self._session.queries))

async def test_auth_query(self):
client = Client()
sample_query = self._sample_query(client.lending_schema)
await client.auth_query(sample_query)
self.assertEquals(
client.bearer, self._session.return_values["signInWithEthereum"]
)

def _sample_query(self, schema):
return DSLQuery(
schema.Query.listOffers.select(schema.OfferConnection.totalCount)
)
Empty file added test/common_utils/__init__.py
Empty file.
File renamed without changes.
File renamed without changes.
51 changes: 51 additions & 0 deletions test/mocks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import copy
from typing import Any

from gql import Client
from gql.dsl import DSLQuery

from gondi.common_utils.singleton import Singleton


class MockedAccount:
def __init__(self):
self._enabled_hd_wallet = False
Expand Down Expand Up @@ -48,3 +57,45 @@ def to_hex(self, data: bytes) -> str:

def keccak(self, _: str) -> bytes:
return b"0"


class MockedSession(metaclass=Singleton):
def __init__(self):
self._return_values = {
"generateSignInNonce": 1,
"signInWithEthereum": "test_bearer",
}

self._queries = []

@property
def queries(self):
return self._queries

@property
def return_values(self) -> dict[str, Any]:
return self._return_values

async def execute(self, query: "DSLQuery") -> dict[str, Any]:
self._queries.append(query)
return copy.copy(self._return_values)


class MockedGraphqlClient:
def __init__(self, schema, transport):
self._client = Client(schema=schema, transport=transport)
self._transport = transport

async def __aenter__(self):
return MockedSession()

async def __aexit__(self, *args, **kwargs):
pass

@property
def schema(self):
return self._client.schema

@property
def transport(self):
return self._transport

0 comments on commit 0a035a8

Please sign in to comment.