From a9295674643890d3613cb2120d01a0d0dda5d61f Mon Sep 17 00:00:00 2001 From: m-kus <44951260+m-kus@users.noreply.github.com> Date: Tue, 2 Jul 2019 18:41:13 +0300 Subject: [PATCH] sqlachemy-like syntax, more examples and tests, poetry --- .gitignore | 2 + LICENSE | 2 +- conseil/__init__.py | 4 + conseil/api.py | 49 +++ conseil/core.py | 197 ++++++++++ conseil/query.py | 237 ++++++++++++ conseilpy/__init__.py | 5 - conseilpy/api.py | 51 --- conseilpy/conseil.py | 327 ---------------- conseilpy/data_types.py | 67 ---- conseilpy/exceptions.py | 4 - conseilpy/utils.py | 6 - example.py | 23 -- ...ated_accounts_which_are_smart_contracts.py | 14 + .../bakers_by_block_count_in_april_2019.py | 14 + ...nd_user_transaction_count_in_april_2019.py | 19 + ...ck_level_transaction_kind_in_april_2019.py | 15 + ...r_of_transactions_by_type_in_april_2019.py | 14 + examples/top_100_transactions_in_2019.py | 15 + examples/top_10_contract_originators.py | 15 + examples/top_10_most_active_contracts.py | 16 + examples/top_20_account_controllers.py | 15 + .../top_20_account_controllers_by_balance.py | 14 + examples/top_20_bakers_by_roll_count.py | 14 + .../top_50_bakers_by_delegator_balance.py | 14 + examples/top_50_bakers_by_delegator_count.py | 14 + pyproject.toml | 24 ++ requirements.txt | 6 - setup.py | 19 - tests/__init__.py | 0 tests/conseil_test.py | 366 ------------------ tests/mock_api.py | 68 ++-- tests/test_metadata.py | 50 +++ tests/test_query.py | 20 + tests/utils_test.py | 24 -- 35 files changed, 801 insertions(+), 943 deletions(-) create mode 100644 conseil/__init__.py create mode 100644 conseil/api.py create mode 100644 conseil/core.py create mode 100644 conseil/query.py delete mode 100644 conseilpy/__init__.py delete mode 100644 conseilpy/api.py delete mode 100644 conseilpy/conseil.py delete mode 100644 conseilpy/data_types.py delete mode 100644 conseilpy/exceptions.py delete mode 100644 conseilpy/utils.py delete mode 100644 example.py create mode 100644 examples/all_originated_accounts_which_are_smart_contracts.py create mode 100644 examples/bakers_by_block_count_in_april_2019.py create mode 100644 examples/blocks_by_end_user_transaction_count_in_april_2019.py create mode 100644 examples/fees_by_block_level_transaction_kind_in_april_2019.py create mode 100644 examples/number_of_transactions_by_type_in_april_2019.py create mode 100644 examples/top_100_transactions_in_2019.py create mode 100644 examples/top_10_contract_originators.py create mode 100644 examples/top_10_most_active_contracts.py create mode 100644 examples/top_20_account_controllers.py create mode 100644 examples/top_20_account_controllers_by_balance.py create mode 100644 examples/top_20_bakers_by_roll_count.py create mode 100644 examples/top_50_bakers_by_delegator_balance.py create mode 100644 examples/top_50_bakers_by_delegator_count.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py create mode 100644 tests/__init__.py delete mode 100644 tests/conseil_test.py create mode 100644 tests/test_metadata.py create mode 100644 tests/test_query.py delete mode 100644 tests/utils_test.py diff --git a/.gitignore b/.gitignore index d33b71c..18a87be 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,5 @@ venv.bak/ .mypy_cache/ api_key +.idea/ +*.ipynb \ No newline at end of file diff --git a/LICENSE b/LICENSE index 8150dd7..08b4804 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Baking Bad +Copyright (c) 2019 Tezos Baking Bad Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/conseil/__init__.py b/conseil/__init__.py new file mode 100644 index 0000000..e8f443e --- /dev/null +++ b/conseil/__init__.py @@ -0,0 +1,4 @@ +from conseil.api import ConseilApi +from conseil.core import Client + +conseil = Client() diff --git a/conseil/api.py b/conseil/api.py new file mode 100644 index 0000000..2ac682a --- /dev/null +++ b/conseil/api.py @@ -0,0 +1,49 @@ +import requests + + +class ConseilException(Exception): + pass + + +class ConseilApi: + + def __init__(self, + api_key='bakingbad', + api_host='https://conseil-dev.cryptonomic-infra.tech', + api_version=2, + timeout=15): + self._api_key = api_key + self._api_host = api_host + self._api_version = api_version + self._timeout = timeout + + def _request(self, method, path, params=None, json=None): + if isinstance(params, dict): + params = {k: v for k, v in params.items() if v is not None} + + response = requests.request( + method=method, + url=f'{self._api_host}/v{self._api_version}/{path}', + params=params, + headers={'apiKey': self._api_key}, + timeout=self._timeout, + json=json + ) + if response.status_code != 200: + raise ConseilException(f'[{response.status_code}]: {response.text}') + + return response + + def get(self, path, **kwargs): + return self._request( + method='GET', + path=path, + params=kwargs + ) + + def post(self, path, json): + return self._request( + method='POST', + path=path, + json=json + ) diff --git a/conseil/core.py b/conseil/core.py new file mode 100644 index 0000000..a912911 --- /dev/null +++ b/conseil/core.py @@ -0,0 +1,197 @@ +from conseil.query import MetadataQuery, DataQuery +from conseil.api import ConseilException +from decimal import Decimal + + +def not_(predicate: dict): + res = predicate.copy() + res['inverse'] = True + return res + + +class Attribute(MetadataQuery): + __query_path__ = 'metadata/{platform_id}/{network_id}/{entity_id}/{attribute_id}' + + def __hash__(self): + return id(self) + + def __getattr__(self, item): + return item + + def query(self) -> DataQuery: + """ + Request specific attribute + :return: DataQuery + """ + kwargs = self._extend(attributes={self['attribute_id']: self}) + return DataQuery(self._api, **kwargs) + + def _predicate(self, operation, *args, inverse=False): + predicate = { + 'field': self['attribute_id'], + 'operation': operation, + 'set': list(args), + 'inverse': inverse + } + + if args and all(map(lambda x: isinstance(x, Decimal), args)): + precision = max(map(lambda x: max(0, -x.as_tuple().exponent), args)) + predicate['precision'] = precision + + return predicate + + def _sort_order(self, direction): + return { + 'field': self['attribute_id'], + 'direction': direction + } + + def _aggregate(self, function): + return self._spawn(aggregation={ + 'field': self['attribute_id'], + 'function': function + }) + + def in_(self, *args): + if len(args) == 0: + return self._is(None) + if len(args) == 1: + return self._is(args[0]) + return self._predicate('in', *args) + + def notin_(self, *args): + if len(args) == 0: + return self._isnot(None) + if len(args) == 1: + return self._isnot(args[0]) + return self._predicate('in', *args, inverse=True) + + def is_(self, value): + if value is None: + return self._predicate('isnull') + return self._predicate('eq', value) + + def isnot(self, value): + if value is None: + return self._predicate('isnull', inverse=True) + return self._predicate('eq', value, inverse=True) + + def between(self, first, second): + return self._predicate('between', first, second) + + def like(self, value): + return self._predicate('like', value) + + def notlike(self, value): + return self._predicate('like', value, inverse=False) + + def __lt__(self, other): + return self._predicate('lt', other) + + def __ge__(self, other): + return self._predicate('lt', other, inverse=True) + + def __gt__(self, other): + return self._predicate('gt', other) + + def __le__(self, other): + return self._predicate('gt', other, inverse=True) + + def __eq__(self, other): + return self.is_(other) + + def __ne__(self, other): + return self.isnot(other) + + def startswith(self, value): + return self._predicate('startsWith', value) + + def endswith(self, value): + return self._predicate('endsWith', value) + + def before(self, value): + return self._predicate('before', value) + + def after(self, value): + return self._predicate('after', value) + + def asc(self): + return self._sort_order('asc') + + def desc(self): + return self._sort_order('desc') + + def sum(self): + return self._aggregate('sum') + + def count(self): + return self._aggregate('count') + + def avg(self): + return self._aggregate('avg') + + def min(self): + return self._aggregate('min') + + def max(self): + return self._aggregate('max') + + +class Entity(MetadataQuery): + __child_key__ = 'attribute_id' + __child_class__ = Attribute + __query_path__ = 'metadata/{platform_id}/{network_id}/{entity_id}/attributes' + + def query(self, *args) -> DataQuery: + """__query_path__ + Request an entity or specific fields + :param args: Array of attributes (of a common entity) or a single entity + :return: DataQuery + """ + if all(map(lambda x: isinstance(x, Attribute), args)): + attributes = {x['attribute_id']: x for x in args} + if any(map(lambda x: x['entity_id'] != self['entity_id'], args)): + raise ConseilException('Entity mismatch') + else: + raise ConseilException('List of attributes (single entity) or an entity is allowed') + kwargs = self._extend(attributes=attributes) + return DataQuery(self._api, **kwargs) + + +class Network(MetadataQuery): + __child_key__ = 'entity_id' + __child_class__ = Entity + __query_path__ = 'metadata/{platform_id}/{network_id}/entities' + + def query(self, *args) -> DataQuery: + """ + Request an entity or specific fields + :param args: Array of attributes (of a common entity) or a single entity + :return: DataQuery + """ + if all(map(lambda x: isinstance(x, Attribute), args)): + attributes = {x['attribute_id']: x for x in args} + if any(map(lambda x: x['entity_id'] != args[0]['entity_id'], args)): + raise ConseilException('Mixed entities') + elif len(args) == 1 and isinstance(args[0], Entity): + attributes = dict() + else: + raise ConseilException('List of attributes (single entity) or an entity is allowed') + + kwargs = self._extend( + attributes=attributes, + entity_id=args[0]['entity_id'] + ) + return DataQuery(self._api, **kwargs) + + +class Platform(MetadataQuery): + __child_key__ = 'network_id' + __child_class__ = Network + __query_path__ = 'metadata/{platform_id}/networks' + + +class Client(MetadataQuery): + __child_key__ = 'platform_id' + __child_class__ = Platform + __query_path__ = 'metadata/platforms' diff --git a/conseil/query.py b/conseil/query.py new file mode 100644 index 0000000..2ae3306 --- /dev/null +++ b/conseil/query.py @@ -0,0 +1,237 @@ +import re +from os.path import basename +from functools import lru_cache +from pprint import pformat + +from conseil.api import ConseilApi, ConseilException + + +def get_attr_docstring(class_type, attr_name): + attr = getattr(class_type, attr_name, None) + if attr and attr.__doc__: + return re.sub(r' {3,}', '', attr.__doc__) + + +class Query: + __query_path__ = None + + def __init__(self, api=ConseilApi(), **kwargs): + self._api = api + self._kwargs = kwargs + + def _extend(self, **kwargs): + params = self._kwargs.copy() + for key, value in kwargs.items(): + if isinstance(params.get(key), list): + params[key].extend(value) + else: + params[key] = value + return params + + def _spawn(self, **kwargs): + params = self._extend(**kwargs) + return self.__class__(self._api, **params) + + @property + def _query_path(self): + return self.__query_path__.format(**self._kwargs) + + def __getitem__(self, item): + return self._kwargs.get(item) + + def __repr__(self): + docstring = '' + if self.__query_path__: + docstring += f'Path\n{self._query_path}\n\n' + + attrs = filter(lambda x: not x.startswith('_'), dir(self.__class__)) + for attr in attrs: + if type(getattr(self.__class__, attr)) == property: + name = f'.{attr}' + else: + name = f'.{attr}()' + info = get_attr_docstring(self.__class__, attr) or '' + docstring += f'{name}{info}\n' + + return docstring + + +class MetadataQuery(Query): + __child_key__ = None + __child_class__ = None + + @lru_cache(maxsize=128) + def _request(self): + try: + if self.__query_path__: + return self._api.get(self._query_path).json() + except ConseilException: + pass + return list() + + @property + def _attr_names(self): + return filter(lambda x: x, + map(lambda x: x.get('name', x) if isinstance(x, dict) else x, + self._request())) + + def __repr__(self): + docstring = super(MetadataQuery, self).__repr__() + + attrs = '\n'.join(map(lambda x: f'.{x}', self._attr_names)) + if attrs: + title = basename(self._query_path).capitalize() + docstring += f'{title}\n{attrs}\n' + + return docstring + + def __call__(self): + return self._request() + + def __dir__(self): + return list(super(MetadataQuery, self).__dir__()) + list(self._attr_names) + + @lru_cache(maxsize=128) + def __getattr__(self, item): + if self.__child_class__: + kwargs = { + self.__child_key__: item, + **self._kwargs + } + return self.__child_class__(self._api, **kwargs) + raise ConseilException(item) + + +class DataQuery(Query): + __query_path__ = 'data/{platform_id}/{network_id}/{entity_id}' + + def __repr__(self): + docstring = super(DataQuery, self).__repr__() + docstring += f'Query\n{pformat(self.payload())}\n\n' + return docstring + + def payload(self): + """ + Resulting Conseil query + :return: object + """ + attributes = self['attributes'] or {} + having = self['having'] or [] + orders = self['order_by'] or [] + + for predicate in having: + try: + attributes[predicate['field']]['aggregation']['predicate'] = predicate + except (KeyError, TypeError): + raise ConseilException(f'Orphan HAVING predicate on `{predicate["field"]}`') + + aggregation = [x['aggregation'] for x in attributes.values() if x['aggregation']] + + for order in orders: + try: + function = attributes[order['field']]['aggregation']['function'] + order['field'] = f'{function}_{order["field"]}' + except (KeyError, TypeError): + pass + + return { + 'fields': list(attributes.keys()), + 'predicates': list(self['predicates'] or []), + 'aggregation': aggregation, + 'orderBy': orders, + 'limit': self['limit'], + 'output': self['output'] or 'json' + } + + def filter(self, *args): + """ + Use predicates to filter results (conjunction) + :param args: array of predicates + :return: DataQuery + """ + return self._spawn(predicates=args) + + def filter_by(self, **kwargs): + """ + Use simple `eq` predicates to filter results (conjunction) + :param kwargs: pairs = + :return: DataQuery + """ + predicates = [ + { + 'field': k, + 'operation': 'eq', + 'set': [v] + } + for k, v in kwargs.items() + ] + return self._spawn(predicates=predicates) + + def order_by(self, *args): + """ + Sort results by specified columns + :param args: one or many sort rules + :return: DataQuery + """ + order_by = [x if isinstance(x, dict) else x.asc() for x in args] + return self._spawn(order_by=order_by) + + def limit(self, limit: int): + """ + Limit results count + :param limit: integer + :return: DataQuery + """ + return self._spawn(limit=limit) + + def having(self, *args): + """ + Filter results by aggregated column + :param args: array of predicates on aggregated columns + :return: DataQuery + """ + return self._spawn(having=args) + + def all(self, output='json'): + """ + Get all results + :param output: output format (json/csv), default is JSON + :return: list (json) or string (csv) + """ + self._kwargs['output'] = output + res = self._api.post(path=self._query_path, json=self.payload()) + if output == 'json': + return res.json() + return res.text + + def one(self): + """ + Get single result, fail if there are no or multiple records (json only) + :return: object + """ + res = self.all() + if len(res) == 0: + raise ConseilException('Not found') + if len(res) > 1: + raise ConseilException('Multiple results') + return res[0] + + def one_or_none(self): + """ + Get single result or None (do not fail) + :return: object or None + """ + try: + return self.one() + except ConseilException: + pass + + def scalar(self): + """ + Get value of a single attribute (single column, single row) + :return: scalar + """ + res = self.one() + if len(res) != 1: + raise ConseilException('Multiple keys') + return next(iter(res.values())) diff --git a/conseilpy/__init__.py b/conseilpy/__init__.py deleted file mode 100644 index af98271..0000000 --- a/conseilpy/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .conseil import * -from .api import * -from .data_types import * -from .exceptions import * - \ No newline at end of file diff --git a/conseilpy/api.py b/conseilpy/api.py deleted file mode 100644 index cabf940..0000000 --- a/conseilpy/api.py +++ /dev/null @@ -1,51 +0,0 @@ -import requests -from requests.exceptions import Timeout -from loguru import logger -from typing import List -from enum import Enum -from cachetools import cached, TTLCache - -from .exceptions import ConseilException - - -__all__ = ["ConseilApi"] - - -class ConseilApi: - __url__ = "https://conseil-dev.cryptonomic-infra.tech" - - def __init__(self, api_key: str, api_version=None, timeout=None): - self._api_key = api_key - self._api_version = api_version if api_version is not None else 2 - self._timeout = timeout if timeout is not None else 15 - - def get(self, command, **kwargs): - request_data = {key: value for key, value in kwargs.items() if value is not None} - url = f'{self.__url__}/v{self._api_version}/{command}' - - headers = { - 'apiKey': self._api_key - } - - try: - response = requests.get(url, params=request_data, headers=headers, timeout=self._timeout) - if response.status_code != 200: - raise ConseilException(f'Invalid response: {response.text} ({response.status_code})') - return response.json() - except Exception as e: - raise ConseilException(e) - - def post(self, command, data): - url = f'{self.__url__}/v{self._api_version}/{command}' - - headers = { - 'apiKey': self._api_key - } - - try: - response = requests.post(url, json=data, headers=headers, timeout=self._timeout) - if response.status_code != 200: - raise ConseilException(f'Invalid response: {response.text} ({response.status_code})') - return response.json() - except Exception as e: - raise ConseilException(e) \ No newline at end of file diff --git a/conseilpy/conseil.py b/conseilpy/conseil.py deleted file mode 100644 index 42eacd7..0000000 --- a/conseilpy/conseil.py +++ /dev/null @@ -1,327 +0,0 @@ -import requests -from requests.exceptions import Timeout -from loguru import logger -from typing import List -from enum import Enum -from cachetools import cached, TTLCache - -from .data_types import Attribute, Entity -from .exceptions import ConseilException -from .api import ConseilApi -from .utils import prepare_name - - -__all__ = ["OutputType", "AggMethod", "Conseil"] - - -class OutputType(Enum): - CSV = "csv" - JSON = "json" - - -class AggMethod(Enum): - SUM = "sum" - COUNT = "count" - MAX = "max" - MIN = "min" - AVG = "avg" - - -class Conseil: - __url__ = "https://conseil-dev.cryptonomic-infra.tech" - - def __init__(self, api: ConseilApi, platform=None, network=None): - self._api = api - self._default_platform = platform - self._default_network = network - self._init_entities() - self._reset_temp() - - def _reset_temp(self): - self._query_platform = None - self._query_network = None - self._temp_request = '' - self._temp_query = { - 'fields': [], - 'predicates': [], - 'limit': 5 - } - self._temp_entity = None - - def _init_entities(self): - entities = self.entities() - for x in entities: - self.__setattr__(prepare_name(x['displayName']), Entity(self._api, self._default_platform, self._default_network, **x)) - - @cached(cache=TTLCache(maxsize=4096, ttl=600)) - def platforms(self): - return self._api.get('metadata/platforms') - - @cached(cache=TTLCache(maxsize=4096, ttl=600)) - def networks(self, platform_name=None): - if platform_name is None: - platform_name = self._default_platform - return self._api.get(f'metadata/{platform_name}/networks') - - @cached(cache=TTLCache(maxsize=4096, ttl=600)) - def entities(self, platform_name=None, network=None): - if platform_name is None: - platform_name = self._default_platform - if network is None: - network = self._default_network - return self._api.get(f'metadata/{platform_name}/{network}/entities') - - @cached(cache=TTLCache(maxsize=4096, ttl=600)) - def attributes(self, entity: str, platform_name=None, network=None): - if platform_name is None: - platform_name = self._default_platform - if network is None: - network = self._default_network - return self._api.get(f'metadata/{platform_name}/{network}/{entity}/attributes') - - @cached(cache=TTLCache(maxsize=4096, ttl=600)) - def attribute_values(self, entity: str, attribute: str, platform_name=None, network=None): - if platform_name is None: - platform_name = self._default_platform - if network is None: - network = self._default_network - return self._api.get(f'metadata/{platform_name}/{network}/{entity}/{attribute}/kind') - - @cached(cache=TTLCache(maxsize=4096, ttl=600)) - def attribute_values_with_prefix(self, - entity: str, - attribute: str, - prefix: str, - platform_name=None, - network=None): - if platform_name is None: - platform_name = self._default_platform - if network is None: - network = self._default_network - return self._api.get(f'metadata/{platform_name}/{network}/{entity}/{attribute}/{prefix}') - - def platform(self, name: str): - self._query_platform = name - return self - - def network(self, name: str): - self._query_network = name - return self - - def query(self, entity: Entity): - if self._query_platform is None: - if self._default_platform is None: - raise ValueError('You must set platform. Please set `platform` in constructor or use Conseil.platform() method') - self._query_platform = self._default_platform - - if self._query_network is None: - if self._default_network is None: - raise ValueError('You must set network. Please set `network` in constructor or use Conseil.network() method') - self._query_network = self._default_network - - self._temp_request = f'data/{self._query_platform}/{self._query_network}/{entity.name}' - self._temp_entity = entity - return self - - def select(self, fields: List[Attribute]): - self._temp_query['fields'] = [ - x.name - for x in fields - if self._temp_entity.name == x.entity - ] - return self - - def limit(self, limit: int): - self._temp_query['limit'] = limit - return self - - def output(self, output_type: OutputType): - self._temp_query['output'] = output_type.value - return self - - def agg(self, field: Attribute, method: AggMethod): - if 'aggregation' not in self._temp_query: - self._temp_query['aggregation'] = list() - - self._temp_query['aggregation'].append({ - 'field': field.name, - 'function': method.value - }) - return self - - def order_by(self, field: Attribute, direction="asc"): - if direction not in ["asc", "desc"]: - raise ConseilException(f'Invalid sort direction: {direction}. Value must be "asc" or "desc"') - - if 'orderBy' not in self._temp_query: - self._temp_query['orderBy'] = list() - - self._temp_query['orderBy'].append({ - 'field': field.name, - 'direction': direction - }) - return self - - def in_(self, field: Attribute, check_list: list, inverse=False, precision=None): - if len(check_list) < 2: - raise ConseilException("'in' requires two or more elements in check list") - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - req = { - 'field': field.name, - 'operation': 'in', - 'set': check_list, - 'inverse': inverse - } - if precision is not None: - req['precision'] = precision - self._temp_query['predicates'].append(req) - return self - - def between(self, field: Attribute, check_list: list, inverse=False, precision=None): - if len(check_list) != 2: - raise ConseilException("'between' must have exactly two elements in check list") - - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - req = { - 'field': field.name, - 'operation': 'between', - 'set': check_list, - 'inverse': inverse - } - if precision is not None: - req['precision'] = precision - self._temp_query['predicates'].append(req) - return self - - def like(self, field: Attribute, template: str, inverse=False): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - self._temp_query['predicates'].append({ - 'field': field.name, - 'operation': 'like', - 'set': [template], - 'inverse': inverse - }) - return self - - def less_than(self, field: Attribute, value: object, inverse=False, precision=None): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - req = { - 'field': field.name, - 'operation': 'lt', - 'set': [value], - 'inverse': inverse - } - if precision is not None: - req['precision'] = precision - self._temp_query['predicates'].append(req) - return self - - def greater_than(self, field: Attribute, value: object, inverse=False, precision=None): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - req = { - 'field': field.name, - 'operation': 'gt', - 'set': [value], - 'inverse': inverse - } - if precision is not None: - req['precision'] = precision - self._temp_query['predicates'].append(req) - return self - - def equals(self, field: Attribute, value: object, inverse=False, precision=None): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - req = { - 'field': field.name, - 'operation': 'eq', - 'set': [value], - 'inverse': inverse - } - if precision is not None: - req['precision'] = precision - self._temp_query['predicates'].append(req) - return self - - def startsWith(self, field: Attribute, value: str, inverse=False): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - self._temp_query['predicates'].append({ - 'field': field.name, - 'operation': 'startsWith', - 'set': [value], - 'inverse': inverse - }) - return self - - def endsWith(self, field: Attribute, value: str, inverse=False): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - self._temp_query['predicates'].append({ - 'field': field.name, - 'operation': 'endsWith', - 'set': [value], - 'inverse': inverse - }) - return self - - def before(self, field: Attribute, value: int, inverse=False): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - self._temp_query['predicates'].append({ - 'field': field.name, - 'operation': 'before', - 'set': [value], - 'inverse': inverse - }) - return self - - def after(self, field: Attribute, value: int, inverse=False): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - self._temp_query['predicates'].append({ - 'field': field.name, - 'operation': 'after', - 'set': [value], - 'inverse': inverse - }) - return self - - def isnull(self, field: Attribute, inverse=False): - if 'predicates' not in self._temp_query: - self._temp_query['predicates'] = list() - - self._temp_query['predicates'].append({ - 'field': field.name, - 'operation': 'isnull', - 'set': [], - 'inverse': inverse - }) - return self - - def get(self): - logger.debug(f'Request: {self._temp_request}') - logger.debug(f'Query: {self._temp_query}') - data = list() - try: - data = self._api.post(self._temp_request, self._temp_query) - except ConseilException as e: - logger.exception(e) - finally: - self._reset_temp() - return data diff --git a/conseilpy/data_types.py b/conseilpy/data_types.py deleted file mode 100644 index 97e7287..0000000 --- a/conseilpy/data_types.py +++ /dev/null @@ -1,67 +0,0 @@ -from cachetools import cached, TTLCache - -from .api import ConseilApi -from .utils import prepare_name - -__all__ = ["Attribute", "Entity"] - - -class Attribute: - def __init__(self, **kwargs): - for k, v in kwargs.items(): - self.__setattr__(k, v) - self.__doc__ = self._build_doc() - - def _build_doc(self): - s = '' - if hasattr(self, 'description'): - s += f'{self.description}\r\n' - if hasattr(self, 'name'): - s += f'Attribute name: {self.name}\r\n' - if hasattr(self, 'dataType'): - s += f'Data type: {self.dataType}\r\n' - if hasattr(self, 'cardinality'): - s += f'Cardinality: {self.cardinality}\r\n' - if hasattr(self, 'keyType'): - s += f'Key type: {self.keyType}\r\n' - if hasattr(self, 'placeholder'): - s += f'Placeholder: {self.placeholder}\r\n' - if hasattr(self, 'scale'): - s += f'Scale: {self.scale}\r\n' - if hasattr(self, 'valueMap'): - s += 'Values:\r\n' - for k, v in self.valueMap.items(): - s += f'\t{k}: {v}\r\n' - if hasattr(self, 'dataFormat'): - s += f'Date format: {self.dataFormat}\r\n' - return s - - def __str__(self): - return self.__doc__ - - __repr__ = __str__ - -class Entity: - def __init__(self, api: ConseilApi, platform_name: str, network: str, **kwargs): - self.platform = platform_name - self.network = network - - for k, v in kwargs.items(): - self.__setattr__(k, v) - - for x in api.get(f'metadata/{self.platform}/{self.network}/{self.name}/attributes'): - self.__setattr__(prepare_name(x['displayName']), Attribute(**x)) - - self.__doc__ = self._build_doc() - - def _build_doc(self): - s = self.displayName - if hasattr(self, 'count'): - s += f' count: {self.count}\r\n' - return s - - def __repr__(self): - return self.__doc__ - - def __str__(self): - return self.displayName diff --git a/conseilpy/exceptions.py b/conseilpy/exceptions.py deleted file mode 100644 index 8bae4cc..0000000 --- a/conseilpy/exceptions.py +++ /dev/null @@ -1,4 +0,0 @@ -__all__ = ["ConseilException"] - -class ConseilException(Exception): - pass diff --git a/conseilpy/utils.py b/conseilpy/utils.py deleted file mode 100644 index 5e91eb7..0000000 --- a/conseilpy/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -__all__ = ['prepare_name'] - -def prepare_name(name): - if name is None: - raise ValueError(f'Invalid name: {name}') - return name.replace(' ', '') diff --git a/example.py b/example.py deleted file mode 100644 index 3a41a1a..0000000 --- a/example.py +++ /dev/null @@ -1,23 +0,0 @@ -from loguru import logger - -from conseilpy import Conseil, AggMethod, ConseilApi - - -if __name__ == '__main__': - key = '' - with open('api_key', 'r') as f: - key = f.read() - - api = ConseilApi(key) - c = Conseil(api, platform="tezos", network="alphanet") - - # /v2/data/tezos//accounts - - data = c.query(c.Account). \ - select([c.Account.Address]). \ - startsWith(c.Account.Address, "KT1"). \ - isnull(c.Account.Script, inverse=True). \ - limit(10). \ - get() - - logger.debug(data) diff --git a/examples/all_originated_accounts_which_are_smart_contracts.py b/examples/all_originated_accounts_which_are_smart_contracts.py new file mode 100644 index 0000000..5bc0034 --- /dev/null +++ b/examples/all_originated_accounts_which_are_smart_contracts.py @@ -0,0 +1,14 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Account = conseil.tezos.alphanet.accounts + + query = Account.query(Account.account_id) \ + .filter(Account.account_id.startswith('KT1'), + Account.script.isnot(None)) \ + .limit(10000) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/bakers_by_block_count_in_april_2019.py b/examples/bakers_by_block_count_in_april_2019.py new file mode 100644 index 0000000..a8d2f03 --- /dev/null +++ b/examples/bakers_by_block_count_in_april_2019.py @@ -0,0 +1,14 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Block = conseil.tezos.alphanet.blocks + + query = Block.query(Block.baker, Block.level.count()) \ + .filter(Block.timestamp.between(1554076800000, 1556668799000)) \ + .order_by(Block.level.desc()) \ + .limit(50) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/blocks_by_end_user_transaction_count_in_april_2019.py b/examples/blocks_by_end_user_transaction_count_in_april_2019.py new file mode 100644 index 0000000..1cd2200 --- /dev/null +++ b/examples/blocks_by_end_user_transaction_count_in_april_2019.py @@ -0,0 +1,19 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Operation = conseil.tezos.alphanet.operations + + query = Operation.query(Operation.block_level, Operation.operation_group_hash.count()) \ + .filter(Operation.timestamp.between(1554076800000, 1556668799000), + Operation.kind.in_(Operation.kind.transaction, + Operation.kind.origination, + Operation.kind.delegation, + Operation.kind.activation, + Operation.kind.reveal)) \ + .order_by(Operation.operation_group_hash.desc()) \ + .limit(100) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/fees_by_block_level_transaction_kind_in_april_2019.py b/examples/fees_by_block_level_transaction_kind_in_april_2019.py new file mode 100644 index 0000000..71785b1 --- /dev/null +++ b/examples/fees_by_block_level_transaction_kind_in_april_2019.py @@ -0,0 +1,15 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Operation = conseil.tezos.alphanet.operations + + query = Operation.query(Operation.block_level, Operation.kind, Operation.fee.sum()) \ + .filter(Operation.fee > 0, + Operation.timestamp.between(1554076800000, 1556668799000)) \ + .order_by(Operation.fee.desc()) \ + .limit(100000) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/number_of_transactions_by_type_in_april_2019.py b/examples/number_of_transactions_by_type_in_april_2019.py new file mode 100644 index 0000000..32cd5f3 --- /dev/null +++ b/examples/number_of_transactions_by_type_in_april_2019.py @@ -0,0 +1,14 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Operation = conseil.tezos.alphanet.operations + + query = Operation.query(Operation.kind, Operation.operation_group_hash.count()) \ + .filter(Operation.timestamp.between(1554076800000, 1556668799000)) \ + .order_by(Operation.operation_group_hash.desc()) \ + .limit(20) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/top_100_transactions_in_2019.py b/examples/top_100_transactions_in_2019.py new file mode 100644 index 0000000..b2a5e60 --- /dev/null +++ b/examples/top_100_transactions_in_2019.py @@ -0,0 +1,15 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Operation = conseil.tezos.alphanet.operations + + query = Operation.query(Operation.source, Operation.amount.sum()) \ + .filter(Operation.kind == Operation.kind.transaction, + Operation.timestamp.between(1546300800000, 1577836799000)) \ + .order_by(Operation.amount.desc()) \ + .limit(100) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/top_10_contract_originators.py b/examples/top_10_contract_originators.py new file mode 100644 index 0000000..f48e75f --- /dev/null +++ b/examples/top_10_contract_originators.py @@ -0,0 +1,15 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Operation = conseil.tezos.alphanet.operations + + query = Operation.query(Operation.source, Operation.operation_group_hash.count()) \ + .filter(Operation.kind == Operation.kind.origination, + Operation.script.isnot(None)) \ + .order_by(Operation.operation_group_hash.desc()) \ + .limit(10) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/top_10_most_active_contracts.py b/examples/top_10_most_active_contracts.py new file mode 100644 index 0000000..6fa2241 --- /dev/null +++ b/examples/top_10_most_active_contracts.py @@ -0,0 +1,16 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Operation = conseil.tezos.alphanet.operations + + query = Operation.query(Operation.destination, Operation.operation_group_hash.count()) \ + .filter(Operation.kind == Operation.kind.transaction, + Operation.destination.startswith('KT1'), + Operation.parameters.isnot(None)) \ + .order_by(Operation.operation_group_hash.desc()) \ + .limit(10) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/top_20_account_controllers.py b/examples/top_20_account_controllers.py new file mode 100644 index 0000000..c49637b --- /dev/null +++ b/examples/top_20_account_controllers.py @@ -0,0 +1,15 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Account = conseil.tezos.alphanet.accounts + + query = Account.query(Account.manager, Account.account_id.count()) \ + .filter(Account.balance > 0, + Account.script.is_(None)) \ + .order_by(Account.account_id.desc()) \ + .limit(20) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/top_20_account_controllers_by_balance.py b/examples/top_20_account_controllers_by_balance.py new file mode 100644 index 0000000..cf11196 --- /dev/null +++ b/examples/top_20_account_controllers_by_balance.py @@ -0,0 +1,14 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Account = conseil.tezos.alphanet.accounts + + query = Account.query(Account.manager, Account.balance.sum()) \ + .filter(Account.script.is_(None)) \ + .order_by(Account.balance.desc()) \ + .limit(20) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/top_20_bakers_by_roll_count.py b/examples/top_20_bakers_by_roll_count.py new file mode 100644 index 0000000..9163c52 --- /dev/null +++ b/examples/top_20_bakers_by_roll_count.py @@ -0,0 +1,14 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Roll = conseil.tezos.alphanet.rolls + + query = Roll.query(Roll.pkh, Roll.rolls) \ + .order_by(Roll.block_level.desc(), + Roll.rolls.desc()) \ + .limit(20) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/top_50_bakers_by_delegator_balance.py b/examples/top_50_bakers_by_delegator_balance.py new file mode 100644 index 0000000..cd1239d --- /dev/null +++ b/examples/top_50_bakers_by_delegator_balance.py @@ -0,0 +1,14 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Account = conseil.tezos.alphanet.accounts + + query = Account.query(Account.delegate_value, Account.balance.sum()) \ + .filter(Account.delegate_value.isnot(None)) \ + .order_by(Account.balance.desc()) \ + .limit(50) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/examples/top_50_bakers_by_delegator_count.py b/examples/top_50_bakers_by_delegator_count.py new file mode 100644 index 0000000..84689bf --- /dev/null +++ b/examples/top_50_bakers_by_delegator_count.py @@ -0,0 +1,14 @@ +from conseil import conseil +from pprint import pprint + + +if __name__ == '__main__': + Account = conseil.tezos.alphanet.accounts + + query = Account.query(Account.delegate_value, Account.account_id.count()) \ + .filter(Account.delegate_value.isnot(None)) \ + .order_by(Account.account_id.desc()) \ + .limit(50) + + pprint(query.payload()) + print(query.all(output='csv')) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e9a2ceb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "conseil" +version = "0.1" +description = "Python toolkit for Conseil blockchain query interface" +authors = ["aopoltorzhicky ", "m-kus <44951260+m-kus@users.noreply.github.com>"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/baking-bad/conseilpy" +keywords = ["tezos", "blockchain"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[tool.poetry.dependencies] +python = "^3.6" +requests = "*" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 64d02b8..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -parameterized==0.7.0 -loguru==0.2.5 -setuptools==39.0.1 -requests==2.22.0 -cachetools==3.0.0 -typing==3.6.6 diff --git a/setup.py b/setup.py deleted file mode 100644 index 2e682f4..0000000 --- a/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="conseilpy", - version="0.0.2", - description="Package for blockchain indexer Conseil", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/baking-bad/conseilpy", - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conseil_test.py b/tests/conseil_test.py deleted file mode 100644 index 107d872..0000000 --- a/tests/conseil_test.py +++ /dev/null @@ -1,366 +0,0 @@ -import unittest -from parameterized import parameterized - -from mock_api import MockApi -from conseilpy.conseil import Conseil, OutputType, AggMethod, ConseilException - -class Test_Conseil(unittest.TestCase): - def __init__(self, *args, **kwargs): - super(Test_Conseil, self).__init__(*args, **kwargs) - self.c = None - - def setUp(cls): - cls.c = Conseil(MockApi(), platform="tezos", network="alphanet") - - def tearDown(cls): - cls.c._reset_temp() - - def test_reset_temp(self): - self.c._query_platform = 'tezos' - self.c._query_network = 'alphanet' - self._temp_request = 'request' - self._temp_query = {} - self._temp_entity = 'entity' - - self.c._reset_temp() - - self.assertEqual(self.c._query_platform, None) - self.assertEqual(self.c._query_network, None) - self.assertEqual(self.c._temp_request, '') - self.assertEqual(self.c._temp_query, { - 'fields': [], - 'predicates': [], - 'limit': 5 - }) - self.assertEqual(self.c._temp_entity, None) - - def test_init_entities(self): - self.c._init_entities() - self.assertIsNotNone(self.c.Accounts) - self.assertEqual(self.c.Accounts.count, 19587) - - def test_platforms(self): - data = self.c.platforms() - self.assertEqual(data, [{ - "name": "tezos", - "displayName": "Tezos" - }]) - - @parameterized.expand([ - (None,), - ("tezos",), - ]) - def test_networks(self, platform): - data = self.c.networks(platform_name=platform) - self.assertEqual(data, [{ - "name": "alphanet", - "displayName": "Alphanet", - "platform": "tezos", - "network": "alphanet" - }]) - - @parameterized.expand([ - (None, None), - ("tezos", None), - (None, 'alphanet'), - ("tezos", "alphanet"), - ]) - def test_entities(self, platform, network): - data = self.c.entities(platform_name=platform, network=network) - self.assertEqual(data, [{ - "name": "accounts", - "displayName": "Accounts", - "count": 19587 - }]) - - @parameterized.expand([ - (None, None, "accounts"), - ("tezos", None, "accounts"), - (None, 'alphanet', "accounts"), - ("tezos", "alphanet", "accounts"), - ]) - def test_attributes(self, platform, network, entity): - data = self.c.attributes(platform_name=platform, network=network, entity=entity) - self.assertEqual(data, [{ - "name": "account_id", - "displayName": "Account id", - "dataType": "String", - "cardinality": 19587, - "keyType": "UniqueKey", - "entity": "accounts" - }, { - "name": "block_id", - "displayName": "Block id", - "dataType": "String", - "cardinality": 4614, - "keyType": "NonKey", - "entity": "accounts" - }]) - - def test_platform(self): - obj = self.c.platform('test') - self.assertEqual(obj._query_platform, 'test') - - def test_network(self): - obj = self.c.network('test') - self.assertEqual(obj._query_network, 'test') - - @parameterized.expand([ - (None, None), - ("tezos", None), - (None, "alphanet"), - ("tezos", "alphanet"), - ]) - def test_query(self, platform, network): - if platform: - self.c.platform(platform) - if network: - self.c.network(network) - - entity = self.c.Accounts - self.c.query(entity) - self.assertEqual(self.c._temp_entity, entity) - self.assertEqual(self.c._temp_request, 'data/tezos/alphanet/accounts') - - def test_select(self): - self.c.query(self.c.Accounts).select([self.c.Accounts.Accountid]) - self.assertEqual(self.c._temp_query['fields'], ['account_id']) - - def test_limit(self): - self.c.query(self.c.Accounts).limit(10) - self.assertEqual(self.c._temp_query['limit'], 10) - - def test_output(self): - self.c.query(self.c.Accounts).output(OutputType.JSON) - self.assertEqual(self.c._temp_query['output'], 'json') - - def test_agg(self): - self.c.query(self.c.Accounts).agg(self.c.Accounts.Accountid, AggMethod.SUM) - self.assertEqual(self.c._temp_query['aggregation'], { - 'field': 'account_id', - 'function': 'sum', - 'predicate': {} - }) - @parameterized.expand([ - ("asc",), - ("desc",), - ("unknown",) - ]) - def test_order_by(self, direction): - if direction in ["asc", "desc"]: - self.c.query(self.c.Accounts).order_by(self.c.Accounts.Accountid, direction) - self.assertEqual(self.c._temp_query['orderBy'], [{ - 'field': "account_id", - 'direction': direction - }]) - else: - with self.assertRaises(ConseilException): - self.c.query(self.c.Accounts).order_by(self.c.Accounts.Accountid, direction) - - @parameterized.expand([ - (["test1", "test2"], False, None), - (["test1", "test2"], True, None), - (["test1"], False, None), - (["test1"], True, None), - (["test1", "test2"], False, 3), - (["test1", "test2"], True, 3), - (["test1"], False, 3), - (["test1"], True, 3), - ]) - def test_in(self, check_list, inverse, precision): - if len(check_list) < 2: - with self.assertRaises(ConseilException): - self.c.query(self.c.Accounts).in_(self.c.Accounts.Accountid, - check_list, inverse, precision) - else: - self.c.query(self.c.Accounts).in_(self.c.Accounts.Accountid, - check_list, inverse, precision) - if precision is None: - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'in', - 'set': check_list, - 'inverse': inverse - }]) - else: - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'in', - 'set': check_list, - 'inverse': inverse, - 'precision': precision - }]) - - @parameterized.expand([ - ([100, 200], False, None), - ([100, 200], True, None), - ([100], False, None), - ([100], True, None), - ([100, 200], False, 3), - ([100, 200], True, 3), - ([100], False, 3), - ([100], True, 3), - ]) - def test_between(self, check_list, inverse, precision): - if len(check_list) != 2: - with self.assertRaises(ConseilException): - self.c.query(self.c.Accounts).between(self.c.Accounts.Accountid, - check_list, inverse, precision) - else: - self.c.query(self.c.Accounts).between(self.c.Accounts.Accountid, - check_list, inverse, precision) - if precision is None: - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'between', - 'set': check_list, - 'inverse': inverse - }]) - else: - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'between', - 'set': check_list, - 'inverse': inverse, - 'precision': precision - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_like(self, inverse): - self.c.query(self.c.Accounts).like(self.c.Accounts.Accountid, "template", inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'like', - 'set': ["template"], - 'inverse': inverse - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_less_than(self, inverse): - self.c.query(self.c.Accounts).less_than(self.c.Accounts.Accountid, 10, inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'lt', - 'set': [10], - 'inverse': inverse - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_greater_than(self, inverse): - self.c.query(self.c.Accounts).greater_than(self.c.Accounts.Accountid, 10, inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'gt', - 'set': [10], - 'inverse': inverse - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_equals(self, inverse): - self.c.query(self.c.Accounts).equals(self.c.Accounts.Accountid, 10, inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'eq', - 'set': [10], - 'inverse': inverse - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_startsWith(self, inverse): - self.c.query(self.c.Accounts).startsWith(self.c.Accounts.Accountid, "10", inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'startsWith', - 'set': ["10"], - 'inverse': inverse - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_endsWith(self, inverse): - self.c.query(self.c.Accounts).endsWith(self.c.Accounts.Accountid, "10", inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'endsWith', - 'set': ["10"], - 'inverse': inverse - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_before(self, inverse): - self.c.query(self.c.Accounts).before(self.c.Accounts.Accountid, 10, inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'before', - 'set': [10], - 'inverse': inverse - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_after(self, inverse): - self.c.query(self.c.Accounts).after(self.c.Accounts.Accountid, 10, inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'after', - 'set': [10], - 'inverse': inverse - }]) - - @parameterized.expand([ - (False,), - (True,), - ]) - def test_isnull(self, inverse): - self.c.query(self.c.Accounts).isnull(self.c.Accounts.Accountid, inverse) - self.assertEqual(self.c._temp_query['predicates'], [{ - 'field': 'account_id', - 'operation': 'isnull', - 'set': [], - 'inverse': inverse - }]) - - @parameterized.expand([ - ('test',), - (None,), - ]) - def test_get(self, uri): - self.c._temp_request = uri - self.c._temp_query = {} - data = self.c.query(self.c.Accounts).get() - self.assertEqual(data, [1]) - self.assertEqual(self.c._query_platform, None) - self.assertEqual(self.c._query_network, None) - self.assertEqual(self.c._temp_request, '') - self.assertEqual(self.c._temp_query, { - 'fields': [], - 'predicates': [], - 'limit': 5 - }) - self.assertEqual(self.c._temp_entity, None) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/mock_api.py b/tests/mock_api.py index ba57218..cad82ea 100644 --- a/tests/mock_api.py +++ b/tests/mock_api.py @@ -1,46 +1,26 @@ -from conseilpy.conseil import ConseilException +from requests import Response +from unittest import TestCase from unittest.mock import MagicMock -__all__ = ['MockApi'] - -responses = { - 'metadata/platforms': [{ - "name": "tezos", - "displayName": "Tezos" - }], - 'metadata/tezos/networks': [{ - "name": "alphanet", - "displayName": "Alphanet", - "platform": "tezos", - "network": "alphanet" - }], - 'metadata/tezos/alphanet/entities': [{ - "name": "accounts", - "displayName": "Accounts", - "count": 19587 - }], - 'metadata/tezos/alphanet/accounts/attributes': [{ - "name": "account_id", - "displayName": "Account id", - "dataType": "String", - "cardinality": 19587, - "keyType": "UniqueKey", - "entity": "accounts" - }, { - "name": "block_id", - "displayName": "Block id", - "dataType": "String", - "cardinality": 4614, - "keyType": "NonKey", - "entity": "accounts" - }] -} - -class MockApi(MagicMock): - def get(self, uri: str): - return responses.get(uri) - - def post(self, uri, data): - if uri is None: - raise ConseilException('test') - return [1] +from conseil.core import Client + + +class MockResponse(Response): + + def json(self): + return [] + + def text(self): + return '' + + +class ConseilCase(TestCase): + + def setUp(self): + self.api = MagicMock() + self.api.get.return_value = MockResponse() + self.api.post.return_value = MockResponse() + self.conseil = Client(self.api) + + def assertLastGetPathEquals(self, path): + self.assertEqual(path, self.api.get.call_args_list[-1][0][0]) diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..ba0f3a0 --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,50 @@ +from conseil.core import * +from tests.mock_api import ConseilCase + + +class MetadataTest(ConseilCase): + + def test_metadata_platforms(self): + self.conseil() + self.assertLastGetPathEquals('metadata/platforms') + self.assertTrue(isinstance(self.conseil, Client)) + + def test_metadata_networks(self): + self.conseil.tezos() + self.assertLastGetPathEquals('metadata/tezos/networks') + self.assertEqual('tezos', self.conseil.tezos['platform_id']) + self.assertTrue(isinstance(self.conseil.tezos, Platform)) + self.assertEqual(id(self.conseil.tezos), id(self.conseil.tezos)) + + def test_metadata_entities(self): + self.conseil.tezos.alphanet() + self.assertLastGetPathEquals('metadata/tezos/alphanet/entities') + self.assertEqual('tezos', self.conseil.tezos.alphanet['platform_id']) + self.assertEqual('alphanet', self.conseil.tezos.alphanet['network_id']) + self.assertTrue(isinstance(self.conseil.tezos.aplhanet, Network)) + self.assertEqual(id(self.conseil.tezos.alphanet), id(self.conseil.tezos.alphanet)) + + def test_metadata_attributes(self): + self.conseil.tezos.alphanet.operations() + self.assertLastGetPathEquals('metadata/tezos/alphanet/operations/attributes') + self.assertEqual('tezos', self.conseil.tezos.alphanet.operations['platform_id']) + self.assertEqual('alphanet', self.conseil.tezos.alphanet.operations['network_id']) + self.assertEqual('operations', self.conseil.tezos.alphanet.operations['entity_id']) + self.assertTrue(isinstance(self.conseil.tezos.alphanet.operations, Entity)) + self.assertEqual(id(self.conseil.tezos.alphanet.operations), + id(self.conseil.tezos.alphanet.operations)) + + def test_metadata_values(self): + self.conseil.tezos.alphanet.operations.kind() + self.assertLastGetPathEquals('metadata/tezos/alphanet/operations/kind') + self.assertEqual('tezos', self.conseil.tezos.alphanet.operations.kind['platform_id']) + self.assertEqual('alphanet', self.conseil.tezos.alphanet.operations.kind['network_id']) + self.assertEqual('operations', self.conseil.tezos.alphanet.operations.kind['entity_id']) + self.assertEqual('kind', self.conseil.tezos.alphanet.operations.kind['attribute_id']) + self.assertTrue(isinstance(self.conseil.tezos.alphanet.operations.kind, Attribute)) + self.assertEqual(id(self.conseil.tezos.alphanet.operations.kind), + id(self.conseil.tezos.alphanet.operations.kind)) + + def test_metadata_terminator(self): + value = self.conseil.tezos.alphanet.operations.kind.transaction + self.assertEqual('transaction', value) diff --git a/tests/test_query.py b/tests/test_query.py new file mode 100644 index 0000000..b4f7475 --- /dev/null +++ b/tests/test_query.py @@ -0,0 +1,20 @@ +from conseil.core import * +from tests.mock_api import ConseilCase + + +class QueryTest(ConseilCase): + + def test_network_query(self): + c = self.conseil.tezos.alphanet + + query = c.query(c.accounts) + self.assertListEqual([], query.payload()['fields']) + + query = c.query(c.accounts.account_id, c.accounts.balance) + self.assertListEqual(['account_id', 'balance'], query.payload()['fields']) + + self.assertRaises(ConseilException, c.query, c.accounts.account_id, c.operations.kind) + self.assertRaises(ConseilException, c.query, 'account_id') + + def test_entity_query(self): + pass diff --git a/tests/utils_test.py b/tests/utils_test.py deleted file mode 100644 index 4c9069e..0000000 --- a/tests/utils_test.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest -from parameterized import parameterized -from loguru import logger - -from conseilpy.utils import prepare_name - - -class Test_Utils(unittest.TestCase): - @parameterized.expand([ - ("tezos", "tezos"), - ("tezos name", "tezosname"), - ("tezos name data", "tezosnamedata"), - (None, None), - ]) - def test_prepare_name(self, name, expected): - if expected is not None: - self.assertEqual(prepare_name(name), expected) - else: - with self.assertRaises(ValueError): - prepare_name(name) - - -if __name__ == '__main__': - unittest.main()