├── setup.cfg ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── matchbook ├── __init__.py ├── tests │ ├── resources │ │ ├── account_balance.json │ │ ├── account_casino_balance.json │ │ ├── account_wallet_transfer.json │ │ ├── betting_positions.json │ │ ├── referencedata_oddstype.json │ │ ├── betting_agg_matched_bets.json │ │ ├── account_full.json │ │ ├── referencedata_currencies.json │ │ ├── betting_send_order.json │ │ ├── betting_amend_order.json │ │ ├── betting_delete_order.json │ │ ├── referencedata_regions.json │ │ ├── referencedata_sports.json │ │ ├── betting_orders.json │ │ ├── betting_delete_bulk.json │ │ ├── marketdata_runners.json │ │ ├── marketdata_markets.json │ │ ├── marketdata_events.json │ │ ├── referencedata_countries.json │ │ └── referencedata_navigation.json │ ├── test_apiclient.py │ ├── test_baseclient.py │ ├── test_account.py │ ├── test_mbapi.py │ ├── test_referencedata.py │ ├── test_reporting.py │ ├── test_betting.py │ └── test_marketdata.py ├── endpoints │ ├── __init__.py │ ├── logout.py │ ├── login.py │ ├── keepalive.py │ ├── account.py │ ├── baseendpoint.py │ ├── referencedata.py │ ├── reporting.py │ ├── marketdata.py │ └── betting.py ├── compat.py ├── apiclient.py ├── resources │ ├── __init__.py │ ├── referencedataresources.py │ ├── accountresources.py │ ├── bettingresources.py │ ├── marketdataresources.py │ ├── baseresource.py │ └── reportresources.py ├── exceptions.py ├── utils.py ├── baseclient.py ├── enums.py └── metadata.py ├── dockerfile ├── README.md ├── setup.py ├── LICENSE └── examples └── matchbook_api_example.ipynb /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.13.0 2 | python-dateutil==2.6.0 3 | pytz==2017.2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.cache/ 3 | *__pycache__* 4 | *.pyc 5 | 6 | build/ 7 | dist/ 8 | matchbook.egg-info/ 9 | *.log 10 | *.html 11 | .vs/ -------------------------------------------------------------------------------- /matchbook/__init__.py: -------------------------------------------------------------------------------- 1 | from matchbook.apiclient import APIClient 2 | from matchbook.exceptions import MBError 3 | 4 | 5 | __title__ = 'matchbook' 6 | __version__ = '0.0.7' 7 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM python:3.5-slim 3 | ADD . /root/matchbook/ 4 | RUN pip install -r /root/matchbook/requirements.txt 5 | WORKDIR /root/matchbook/ 6 | RUN python setup.py install -------------------------------------------------------------------------------- /matchbook/tests/resources/account_balance.json: -------------------------------------------------------------------------------- 1 | {'Latency': 0.027001, 2 | 'TIMESTAMP': '2017-07-26 17:25:01.818470', 3 | 'balance': 0.0, 4 | 'commission-reserve': 0.0, 5 | 'exposure': 0.0, 6 | 'free-funds': 0.0, 7 | 'id': 1} -------------------------------------------------------------------------------- /matchbook/tests/resources/account_casino_balance.json: -------------------------------------------------------------------------------- 1 | {'Latency': 0.083, 2 | 'TIMESTAMP': '2017-07-26 17:32:02.878805', 3 | 'balance': 0.0, 4 | 'commission-credit': 0.0, 5 | 'exposure': 0.0, 6 | 'free-funds': 0.0, 7 | 'id': 1} -------------------------------------------------------------------------------- /matchbook/tests/resources/account_wallet_transfer.json: -------------------------------------------------------------------------------- 1 | {'Latency': 0.182001, 2 | 'TIMESTAMP': '2017-07-26 17:33:08.972076', 3 | 'eddie-wallet': {'available': 0.0, 4 | 'balance': 0.0, 5 | 'exposure': 0.0}, 6 | 'matchbook-wallet': {'available': 0.0, 7 | 'balance': 0.0, 8 | 'exposure': 0.0}} 9 | -------------------------------------------------------------------------------- /matchbook/tests/resources/betting_positions.json: -------------------------------------------------------------------------------- 1 | {'offset': 0, 2 | 'per-page': 500, 3 | 'positions': [{'event-id': 553859672020032, 4 | 'market-id': 553859672230032, 5 | 'potential-loss': -7.69231, 6 | 'runner-id': 553859672320109}, 7 | {'event-id': 553859672020032, 8 | 'market-id': 553859672230032, 9 | 'potential-profit': 12.0, 10 | 'runner-id': 553859672320132}], 11 | 'total': 2} 12 | -------------------------------------------------------------------------------- /matchbook/tests/resources/referencedata_oddstype.json: -------------------------------------------------------------------------------- 1 | {'odds-types': [{'odds-type': 'US', 'odds-type-id': 'US'}, 2 | {'odds-type': 'DECIMAL', 'odds-type-id': 'DECIMAL'}, 3 | {'odds-type': '%', 'odds-type-id': 'PROBABILITY'}, 4 | {'odds-type': 'HK', 'odds-type-id': 'HONG_KONG'}, 5 | {'odds-type': 'MALAY', 'odds-type-id': 'MALAY'}, 6 | {'odds-type': 'INDO', 'odds-type-id': 'INDONESIAN'}, 7 | {'odds-type': 'FRAC', 'odds-type-id': 'FRACTIONAL'}]} -------------------------------------------------------------------------------- /matchbook/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | from matchbook.endpoints.login import Login 2 | from matchbook.endpoints.keepalive import KeepAlive 3 | from matchbook.endpoints.logout import Logout 4 | from matchbook.endpoints.betting import Betting 5 | from matchbook.endpoints.account import Account 6 | from matchbook.endpoints.marketdata import MarketData 7 | from matchbook.endpoints.referencedata import ReferenceData 8 | from matchbook.endpoints.reporting import Reporting 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matchbook 2 | Python wrapper for Matchbook API. 3 | 4 | [Matchbook Documentation](https://developers.matchbook.com/v1.0/reference) 5 | 6 | # Installation 7 | 8 | ``` 9 | $ python install matchbook 10 | ``` 11 | 12 | # Usage 13 | 14 | ```python 15 | >>> from matchbook.apiclient import APIClient 16 | >>> api = APIClient('username', 'password') 17 | >>> sport_ids = api.reference_data.get_sports() 18 | >>> tennis_events = api.market_data.get_events(sport_ids=[9]) 19 | ``` 20 | -------------------------------------------------------------------------------- /matchbook/endpoints/logout.py: -------------------------------------------------------------------------------- 1 | from matchbook.endpoints.baseendpoint import BaseEndpoint 2 | from matchbook.exceptions import AuthError 3 | 4 | 5 | class Logout(BaseEndpoint): 6 | 7 | def __call__(self, session=None): 8 | response = self.request("DELETE", self.client.urn_main, 'security/session', data=self.data, session=session) 9 | self.client.set_session_token(None, None) 10 | 11 | @property 12 | def data(self): 13 | return {'username': self.client.username, 'password': self.client.password} 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | from setuptools import setup, find_packages 3 | from matchbook import __version__ 4 | 5 | setup( 6 | name="matchbook", 7 | version=__version__, 8 | author="Rory Cole", 9 | author_email="rory.cole1990@gmail.com", 10 | description="Matchbook API Python wrapper", 11 | url="https://github.com/rozzac90/matchbook", 12 | packages=find_packages(), 13 | install_requires=[line.strip() for line in open("requirements.txt")], 14 | long_description=open('README.md').read(), 15 | tests_require=['pytest'], 16 | ) 17 | -------------------------------------------------------------------------------- /matchbook/tests/resources/betting_agg_matched_bets.json: -------------------------------------------------------------------------------- 1 | {'currency': 'EUR', 2 | 'exchange-type': 'back-lay', 3 | 'language': 'en', 4 | 'matched-bets': [{'decimal-odds': 1.641, 5 | 'event-id': 553859672020032, 6 | 'event-name': 'C Berlocq vs G Melzer', 7 | 'market-id': 553859672230032, 8 | 'market-name': 'Moneyline', 9 | 'odds': 1.641, 10 | 'odds-type': 'DECIMAL', 11 | 'potential-liability': 7.692, 12 | 'runner-id': 553859672320109, 13 | 'runner-name': 'C Berlocq', 14 | 'side': 'lay', 15 | 'stake': 12.0}], 16 | 'odds-type': 'DECIMAL', 17 | 'total': 1} -------------------------------------------------------------------------------- /matchbook/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | _ver = sys.version_info 5 | 6 | #: Python 2.x? 7 | is_py2 = (_ver[0] == 2) 8 | 9 | #: Python 3.x? 10 | is_py3 = (_ver[0] == 3) 11 | 12 | 13 | try: 14 | from builtins import FileNotFoundError 15 | except ImportError: 16 | class FileNotFoundError(OSError): 17 | pass 18 | 19 | 20 | if is_py2: 21 | basestring = basestring 22 | numeric_types = (int, long, float) 23 | integer_types = (int, long) 24 | elif is_py3: 25 | basestring = (str, bytes) 26 | numeric_types = (int, float) 27 | integer_types = (int,) 28 | -------------------------------------------------------------------------------- /matchbook/endpoints/login.py: -------------------------------------------------------------------------------- 1 | 2 | from matchbook.endpoints.baseendpoint import BaseEndpoint 3 | from matchbook.exceptions import AuthError 4 | 5 | 6 | class Login(BaseEndpoint): 7 | 8 | def __call__(self, session=None): 9 | response = self.request("POST", self.client.urn_main, 'security/session', data=self.data, session=session) 10 | response_json = response.json() 11 | self.client.set_session_token(response_json.get('session-token'), response_json.get('user-id')) 12 | 13 | @property 14 | def data(self): 15 | return {'username': self.client.username, 'password': self.client.password} 16 | 17 | -------------------------------------------------------------------------------- /matchbook/tests/resources/account_full.json: -------------------------------------------------------------------------------- 1 | {'Latency': 0.0255, 2 | 'TIMESTAMP': '2017-07-26 17:29:32.212598', 3 | 'balance': 0.0, 4 | 'bet-slip-pinned': True, 5 | 'commission-reserve': 0.0, 6 | 'commission-type': 'VOLUME', 7 | 'country': 'IE', 8 | 'currency': 'EUR', 9 | 'email': 'username@gmail.com', 10 | 'exchange-type': 'back-lay', 11 | 'exposure': 0.0, 12 | 'family-names': 'LastName', 13 | 'free-funds': 0.0, 14 | 'given-names': 'FirstName', 15 | 'id': 1, 16 | 'language': 'en', 17 | 'odds-type': 'DECIMAL', 18 | 'roles': ['USER'], 19 | 'show-bet-confirmation': True, 20 | 'show-odds-rounding-message': True, 21 | 'show-position': True, 22 | 'status': 'ACTIVE', 23 | 'user-id': 1, 24 | 'username': 'username'} -------------------------------------------------------------------------------- /matchbook/apiclient.py: -------------------------------------------------------------------------------- 1 | from matchbook.baseclient import BaseClient 2 | from matchbook import endpoints 3 | 4 | 5 | class APIClient(BaseClient): 6 | 7 | def __init__(self, username, password=None): 8 | super(APIClient, self).__init__(username, password) 9 | 10 | self.login = endpoints.Login(self) 11 | self.keep_alive = endpoints.KeepAlive(self) 12 | self.logout = endpoints.Logout(self) 13 | self.betting = endpoints.Betting(self) 14 | self.account = endpoints.Account(self) 15 | self.market_data = endpoints.MarketData(self) 16 | self.reference_data = endpoints.ReferenceData(self) 17 | self.reporting = endpoints.Reporting(self) 18 | 19 | def __repr__(self): 20 | return '' % self.username 21 | 22 | def __str__(self): 23 | return 'APIClient' 24 | -------------------------------------------------------------------------------- /matchbook/tests/test_apiclient.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from matchbook.apiclient import APIClient 5 | from matchbook.endpoints import Betting, Account, KeepAlive, Login, Logout, MarketData, ReferenceData, Reporting 6 | 7 | 8 | class APIClientTest(unittest.TestCase): 9 | 10 | def test_apiclient_init(self): 11 | client = APIClient('username', 'password') 12 | assert str(client) == 'APIClient' 13 | assert repr(client) == '' 14 | assert isinstance(client.account, Account) 15 | assert isinstance(client.betting, Betting) 16 | assert isinstance(client.keep_alive, KeepAlive) 17 | assert isinstance(client.login, Login) 18 | assert isinstance(client.logout, Logout) 19 | assert isinstance(client.market_data, MarketData) 20 | assert isinstance(client.reference_data, ReferenceData) 21 | assert isinstance(client.reporting, Reporting) 22 | -------------------------------------------------------------------------------- /matchbook/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from matchbook.resources.baseresource import BaseResource 2 | 3 | from matchbook.resources.accountresources import ( 4 | AccountDetails, 5 | AccountBalance, 6 | AccountTransfer, 7 | ) 8 | 9 | from matchbook.resources.bettingresources import ( 10 | Order, 11 | MatchedBets, 12 | Position, 13 | ) 14 | 15 | from matchbook.resources.reportresources import ( 16 | BetReport, 17 | SettlementReport, 18 | MarketSettlementReport, 19 | RunnerSettlementReport, 20 | CommissionReport, 21 | TransactionReport, 22 | SettlementEvent 23 | ) 24 | 25 | from matchbook.resources.referencedataresources import ( 26 | SportsDetails, 27 | OddsType, 28 | Currencies, 29 | Countries, 30 | Regions, 31 | MetaTags, 32 | ) 33 | 34 | from matchbook.resources.marketdataresources import ( 35 | Event, 36 | Market, 37 | EventMeta, 38 | Runner, 39 | Price, 40 | ) -------------------------------------------------------------------------------- /matchbook/tests/resources/referencedata_currencies.json: -------------------------------------------------------------------------------- 1 | {'currencies': [{'currency-id': 8, 2 | 'long-name': 'Australian Dollar', 3 | 'short-name': 'AUD'}, 4 | {'currency-id': 4, 'long-name': 'Canadian Dollar', 'short-name': 'CAD'}, 5 | {'currency-id': 6, 6 | 'long-name': 'Chinese Yuan Renminbi', 7 | 'short-name': 'CNY'}, 8 | {'currency-id': 2, 'long-name': 'Euro', 'short-name': 'EUR'}, 9 | {'currency-id': 3, 'long-name': 'British Pound', 'short-name': 'GBP'}, 10 | {'currency-id': 9, 'long-name': 'Hong Kong Dollar', 'short-name': 'HKD'}, 11 | {'currency-id': 5, 'long-name': 'Indonesian Rupiah', 'short-name': 'IDR'}, 12 | {'currency-id': 7, 'long-name': 'Japanese Yen', 'short-name': 'JPY'}, 13 | {'currency-id': 11, 'long-name': 'South Korea Won', 'short-name': 'KRW'}, 14 | {'currency-id': 10, 'long-name': 'Taiwan New Dollars', 'short-name': 'TWD'}, 15 | {'currency-id': 1, 16 | 'long-name': 'United States Dollars', 17 | 'short-name': 'USD'}], 18 | 'total': 11} 19 | In [ ]: 20 | -------------------------------------------------------------------------------- /matchbook/tests/resources/betting_send_order.json: -------------------------------------------------------------------------------- 1 | {'currency': 'EUR', 2 | 'exchange-type': 'back-lay', 3 | 'language': 'en', 4 | 'odds-type': 'DECIMAL', 5 | 'offers': [{'acceptor-commission-rate': 0.0168, 6 | 'commission-reserve': 0.09231, 7 | 'commission-type': 'VOLUME', 8 | 'created-at': '2017-08-01T08:28:06.071Z', 9 | 'currency': 'EUR', 10 | 'decimal-odds': 1.54945, 11 | 'event-id': 553859672020032, 12 | 'event-name': 'C Berlocq vs G Melzer', 13 | 'exchange-type': 'back-lay', 14 | 'id': 556192146410015, 15 | 'in-play': False, 16 | 'market-id': 553859672230032, 17 | 'market-name': 'Moneyline', 18 | 'market-type': 'money_line', 19 | 'odds': 1.54945, 20 | 'odds-type': 'DECIMAL', 21 | 'originator-commission-rate': 0.0084, 22 | 'potential-liability': 5.49451, 23 | 'remaining': 10, 24 | 'remaining-potential-liability': 5.49451, 25 | 'runner-id': 553859672320109, 26 | 'runner-name': 'C Berlocq', 27 | 'side': 'lay', 28 | 'stake': 10, 29 | 'status': 'open'}]} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rory Cole 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /matchbook/tests/resources/betting_amend_order.json: -------------------------------------------------------------------------------- 1 | {'acceptor-commission-rate': 0.0168, 2 | 'commission-reserve': 0.09545, 3 | 'commission-type': 'VOLUME', 4 | 'created-at': '2017-08-01T08:28:06.071Z', 5 | 'currency': 'EUR', 6 | 'decimal-odds': 1.56818, 7 | 'event-id': 553859672020032, 8 | 'event-name': 'C Berlocq vs G Melzer', 9 | 'exchange-type': 'back-lay', 10 | 'id': 556192146410015, 11 | 'in-play': False, 12 | 'market-id': 553859672230032, 13 | 'market-name': 'Moneyline', 14 | 'market-type': 'money_line', 15 | 'odds': 1.56818, 16 | 'odds-type': 'DECIMAL', 17 | 'offer-edits': [{'decimal-odds-after': 1.56819, 18 | 'decimal-odds-before': 1.54946, 19 | 'edit-time': '2017-08-01T08:30:03.527Z', 20 | 'id': 556193320970115, 21 | 'odds-after': 1.56818, 22 | 'odds-before': 1.54945, 23 | 'odds-type': 'DECIMAL', 24 | 'offer-id': 556192146410015, 25 | 'stake-after': 10, 26 | 'stake-before': 10}], 27 | 'originator-commission-rate': 0.0084, 28 | 'potential-liability': 5.68182, 29 | 'remaining': 10, 30 | 'remaining-potential-liability': 5.68182, 31 | 'runner-id': 553859672320109, 32 | 'runner-name': 'C Berlocq', 33 | 'side': 'lay', 34 | 'stake': 10, 35 | 'status': 'edited'} -------------------------------------------------------------------------------- /matchbook/tests/resources/betting_delete_order.json: -------------------------------------------------------------------------------- 1 | {'acceptor-commission-rate': 0.0168, 2 | 'commission-reserve': 0.09545, 3 | 'commission-type': 'VOLUME', 4 | 'created-at': '2017-08-01T08:28:06.071Z', 5 | 'currency': 'EUR', 6 | 'decimal-odds': 1.56818, 7 | 'event-id': 553859672020032, 8 | 'event-name': 'C Berlocq vs G Melzer', 9 | 'exchange-type': 'back-lay', 10 | 'id': 556192146410015, 11 | 'in-play': False, 12 | 'market-id': 553859672230032, 13 | 'market-name': 'Moneyline', 14 | 'market-type': 'money_line', 15 | 'odds': 1.56818, 16 | 'odds-type': 'DECIMAL', 17 | 'offer-edits': [{'decimal-odds-after': 1.56819, 18 | 'decimal-odds-before': 1.54946, 19 | 'edit-time': '2017-08-01T08:30:03.527Z', 20 | 'id': 556193320970115, 21 | 'odds-after': 1.56818, 22 | 'odds-before': 1.54945, 23 | 'odds-type': 'DECIMAL', 24 | 'offer-id': 556192146410015, 25 | 'stake-after': 10, 26 | 'stake-before': 10}], 27 | 'originator-commission-rate': 0.0084, 28 | 'potential-liability': 5.68182, 29 | 'remaining': 10, 30 | 'remaining-potential-liability': 5.68182, 31 | 'runner-id': 553859672320109, 32 | 'runner-name': 'C Berlocq', 33 | 'side': 'lay', 34 | 'stake': 10, 35 | 'status': 'cancelled'} -------------------------------------------------------------------------------- /matchbook/resources/referencedataresources.py: -------------------------------------------------------------------------------- 1 | from matchbook.resources.baseresource import BaseResource 2 | 3 | 4 | class SportsDetails(BaseResource): 5 | class Meta(BaseResource.Meta): 6 | attributes = { 7 | 'id': 'id', 8 | 'name': 'name', 9 | 'type': 'type', 10 | } 11 | 12 | 13 | class OddsType(BaseResource): 14 | class Meta(BaseResource.Meta): 15 | attributes = { 16 | 'odds-type': 'odds-type', 17 | 'odds-type-id': 'odds-type-id', 18 | } 19 | 20 | 21 | class Currencies(BaseResource): 22 | class Meta(BaseResource.Meta): 23 | attributes = { 24 | 'currency-id': 'currency-id', 25 | 'long-name': 'long-name', 26 | 'short-name': 'short-name', 27 | } 28 | 29 | 30 | class Countries(BaseResource): 31 | class Meta(BaseResource.Meta): 32 | attributes = { 33 | 'country-id': 'country-id', 34 | 'name': 'name', 35 | 'country-code': 'country-code', 36 | } 37 | 38 | 39 | class Regions(BaseResource): 40 | class Meta(BaseResource.Meta): 41 | attributes = { 42 | 'region-id': 'region-id', 43 | 'name': 'name', 44 | } 45 | 46 | 47 | class MetaTags(BaseResource): 48 | class Meta(BaseResource.Meta): 49 | attributes = { 50 | 51 | } 52 | -------------------------------------------------------------------------------- /matchbook/tests/test_baseclient.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import unittest 4 | import requests 5 | 6 | from matchbook.baseclient import BaseClient 7 | from matchbook.exceptions import PasswordError 8 | 9 | 10 | class BaseClientTest(unittest.TestCase): 11 | 12 | def test_baseclient_init(self): 13 | client = BaseClient(username='username', password='password') 14 | assert client.username == 'username' 15 | assert client.password == 'password' 16 | assert client.url == 'https://matchbook.com' 17 | assert client.url_beta == 'https://beta.matchbook.com' 18 | assert client.urn_main == '/bpapi/rest/' 19 | assert client.urn_edge == '/edge/rest/' 20 | assert isinstance(client.session, requests.Session) 21 | assert client.headers == {'Content-Type': 'application/json', 'Accept': 'application/json'} 22 | assert client.exchange_type == 'back-lay' 23 | assert client.currency == 'EUR' 24 | 25 | def test_get_password(self): 26 | if 'MATCHBOOK_PW' in os.environ: 27 | client = BaseClient(username='username') 28 | assert client.password == os.environ['MATCHBOOK_PW'] 29 | else: 30 | with self.assertRaises(PasswordError): 31 | BaseClient(username='username') 32 | 33 | def test_session_token(self): 34 | client = BaseClient(username='username', password='password') 35 | client.set_session_token(session_token='session_token', user_id=1234) 36 | assert client.session_token == 'session_token' 37 | assert client.user_id == 1234 38 | -------------------------------------------------------------------------------- /matchbook/resources/accountresources.py: -------------------------------------------------------------------------------- 1 | from matchbook.resources.baseresource import BaseResource 2 | 3 | 4 | class AccountDetails(BaseResource): 5 | class Meta(BaseResource.Meta): 6 | attributes = { 7 | 'id': 'id', 8 | 'balance': 'balance', 9 | 'bet-slip-pinned': 'bet-slip-pinned', 10 | 'commission-reserve': 'commission-reserve', 11 | 'commission-type': 'commission-type', 12 | 'currency': 'currency', 13 | 'email': 'email', 14 | 'exchange-type': 'exchange-type', 15 | 'exposure': 'exposure', 16 | 'family-names': 'family-names', 17 | 'free-funds': 'free-funds', 18 | 'given-names': 'given-names', 19 | 'language': 'language', 20 | 'odds-type': 'odds-type', 21 | 'roles': 'roles', 22 | 'show-bet-confirmation': 'show-bet-confirmation', 23 | 'show-odds-rounding-message': 'show-odds-rounding-message', 24 | 'show-position': 'show-position', 25 | 'status': 'status', 26 | 'user-id': 'user-id', 27 | 'username': 'username' 28 | } 29 | 30 | 31 | class AccountBalance(BaseResource): 32 | class Meta(BaseResource.Meta): 33 | attributes = { 34 | 'id': 'id', 35 | 'balance': 'balance', 36 | 'commission-reserve': 'commission-reserve', 37 | 'exposure': 'exposure', 38 | 'free-funds': 'free-funds', 39 | } 40 | 41 | 42 | class AccountTransfer(BaseResource): 43 | class Meta(BaseResource.Meta): 44 | attributes = { 45 | 46 | } 47 | -------------------------------------------------------------------------------- /matchbook/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | 4 | if not hasattr(json, 'JSONDecodeError'): 5 | json.JSONDecodeError = ValueError 6 | else: 7 | from json.decoder import JSONDecodeError 8 | 9 | 10 | class MBError(Exception): 11 | pass 12 | 13 | 14 | class NotLoggedIn(MBError): 15 | pass 16 | 17 | 18 | class AuthError(MBError): 19 | def __init__(self, response): 20 | self.response = response 21 | self.status_code = response.status_code 22 | try: 23 | self.message = response.json().get('errors')[0].get('messages') 24 | except: 25 | self.message = 'UNKNOWN' 26 | super(AuthError, self).__init__(self.message) 27 | 28 | 29 | class ApiError(MBError): 30 | def __init__(self, response): 31 | self.response = response 32 | self.status_code = response.status_code 33 | try: 34 | error_data = response.json().get('errors') 35 | self.message = error_data[0].get('messages', 'UNKNOWN') 36 | 37 | except (KeyError, JSONDecodeError, TypeError): 38 | self.message = 'UNKNOWN' 39 | print(response) 40 | super(ApiError, self).__init__(self.message) 41 | 42 | 43 | class PasswordError(MBError): 44 | """Exception raised if password is not found""" 45 | 46 | def __init__(self): 47 | message = 'Password not found in environment variables, add or pass to APIClient' 48 | super(PasswordError, self).__init__(message) 49 | 50 | 51 | class StatusCodeError(MBError): 52 | 53 | def __init__(self, status_code): 54 | message = 'Status code error: %s' % status_code 55 | super(StatusCodeError, self).__init__(message) 56 | 57 | -------------------------------------------------------------------------------- /matchbook/utils.py: -------------------------------------------------------------------------------- 1 | 2 | import datetime 3 | from matchbook.exceptions import ApiError 4 | 5 | 6 | def clean_time(col): 7 | """ 8 | Parse a UTC time column to datetime. 9 | 10 | :param col: column to be parsed. 11 | :returns: datetime column 12 | :rtype: series 13 | 14 | """ 15 | return col.apply(lambda x: datetime.datetime.strptime(x, '%Y-%m-%dT%H:%M:%S.%fZ')) 16 | 17 | 18 | def filter_dicts(d): 19 | """ 20 | Filter dict to remove None values. 21 | 22 | :param d: data to filter 23 | :type d: dict 24 | :returns: filtered data 25 | :rtype: dict 26 | 27 | """ 28 | return dict((k, v) for k, v in d.items() if v is not None) 29 | 30 | 31 | def clean_locals(params): 32 | """ 33 | Clean up locals dict, remove empty and self params. 34 | 35 | :params params: locals dicts from a function. 36 | :type params: dict 37 | :returns: cleaned locals dict to use as params for functions 38 | :rtype: dict 39 | """ 40 | clean_params = dict((k, v) for k, v in params.items() if v is not None and k != 'self' and k != 'session') 41 | for k, v in clean_params.items(): 42 | if '_' in k: 43 | new_key = k.replace('_', '-') 44 | clean_params[new_key] = v 45 | clean_params.pop(k) 46 | return clean_params 47 | 48 | 49 | def check_call_complete(response): 50 | return response.get('total', 0) < (response.get('per-page', 20) + response.get('offset', 0)) 51 | 52 | 53 | def check_status_code(response, codes=None): 54 | """Checks response.status_code is in codes 55 | :param response: Requests response 56 | :param codes: List of accepted codes or callable 57 | :raises: StatusCodeError if code invalid 58 | """ 59 | codes = codes or [200] 60 | if response.status_code not in codes: 61 | raise ApiError(response) 62 | -------------------------------------------------------------------------------- /matchbook/tests/resources/referencedata_regions.json: -------------------------------------------------------------------------------- 1 | {'regions': [{'name': '', 'region-id': 100283}, 2 | {'name': 'Carlow', 'region-id': 972}, 3 | {'name': 'Cavan', 'region-id': 973}, 4 | {'name': 'Clare', 'region-id': 974}, 5 | {'name': 'Cork', 'region-id': 975}, 6 | {'name': 'County Antrim', 'region-code': 'ANTRIM', 'region-id': 100091}, 7 | {'name': 'County Armagh', 'region-code': 'ARMAGH', 'region-id': 100092}, 8 | {'name': 'County Derry', 'region-code': 'DERRY', 'region-id': 100095}, 9 | {'name': 'County Down', 'region-code': 'DOWN', 'region-id': 100093}, 10 | {'name': 'County Fermanagh', 11 | 'region-code': 'FERMANAGH', 12 | 'region-id': 100094}, 13 | {'name': 'County Tyrone', 'region-code': 'TYRONE', 'region-id': 100096}, 14 | {'name': 'Donegal', 'region-id': 976}, 15 | {'name': 'Dublin', 'region-id': 977}, 16 | {'name': 'Galway', 'region-id': 978}, 17 | {'name': 'Kerry', 'region-id': 979}, 18 | {'name': 'Kildare', 'region-id': 980}, 19 | {'name': 'Kilkenny', 'region-id': 981}, 20 | {'name': 'Laois', 'region-id': 982}, 21 | {'name': 'Leitrim', 'region-id': 983}, 22 | {'name': 'Limerick', 'region-id': 984}, 23 | {'name': 'Longford', 'region-id': 985}, 24 | {'name': 'Louth', 'region-id': 986}, 25 | {'name': 'Mayo', 'region-id': 987}, 26 | {'name': 'Meath', 'region-id': 988}, 27 | {'name': 'Monaghan', 'region-id': 989}, 28 | {'name': 'Munster', 'region-code': 'MU', 'region-id': 100066}, 29 | {'name': 'Offaly', 'region-id': 990}, 30 | {'name': 'Other', 'region-id': 100035}, 31 | {'name': 'Roscommon', 'region-id': 991}, 32 | {'name': 'Sligo', 'region-id': 992}, 33 | {'name': 'Tipperary', 'region-id': 993}, 34 | {'name': 'Waterford', 'region-id': 994}, 35 | {'name': 'Westmeath', 'region-id': 995}, 36 | {'name': 'Wexford', 'region-id': 996}, 37 | {'name': 'Wicklow', 'region-id': 997}], 38 | 'total': 35} -------------------------------------------------------------------------------- /matchbook/tests/resources/referencedata_sports.json: -------------------------------------------------------------------------------- 1 | {'offset': 0, 2 | 'per-page': 500, 3 | 'sports': [{'id': 1, 'name': 'American Football', 'type': 'SPORT'}, 4 | {'id': 555636871580009, 'name': 'Athletics', 'type': 'SPORT'}, 5 | {'id': 112, 'name': 'Australian Rules', 'type': 'SPORT'}, 6 | {'id': 13, 'name': 'Auto Racing', 'type': 'SPORT'}, 7 | {'id': 3, 'name': 'Baseball', 'type': 'SPORT'}, 8 | {'id': 4, 'name': 'Basketball', 'type': 'SPORT'}, 9 | {'id': 14, 'name': 'Boxing', 'type': 'SPORT'}, 10 | {'id': 110, 'name': 'Cricket', 'type': 'SPORT'}, 11 | {'id': 11, 'name': 'Current Events', 'type': 'SPORT'}, 12 | {'id': 115, 'name': 'Cycling', 'type': 'SPORT'}, 13 | {'id': 116, 'name': 'Darts', 'type': 'SPORT'}, 14 | {'id': 117, 'name': 'Gaelic Football', 'type': 'SPORT'}, 15 | {'id': 8, 'name': 'Golf', 'type': 'SPORT'}, 16 | {'id': 241798357140019, 'name': 'Greyhound Racing', 'type': 'SPORT'}, 17 | {'id': 24735152712200, 'name': 'Horse Racing', 'type': 'SPORT'}, 18 | {'id': 222109340250019, 'name': 'Horse Racing (Ante Post)', 'type': 'SPORT'}, 19 | {'id': 231138347942400, 'name': 'Horse Racing Beta', 'type': 'SPORT'}, 20 | {'id': 118, 'name': 'Hurling', 'type': 'SPORT'}, 21 | {'id': 6, 'name': 'Ice Hockey', 'type': 'SPORT'}, 22 | {'id': 126, 'name': 'MMA', 'type': 'SPORT'}, 23 | {'id': 5, 'name': 'NCAA Basketball', 'type': 'SPORT'}, 24 | {'id': 2, 'name': 'NCAA Football', 'type': 'SPORT'}, 25 | {'id': 385227477790005, 'name': 'Politics', 'type': 'SPORT'}, 26 | {'id': 114, 'name': 'Rugby League', 'type': 'SPORT'}, 27 | {'id': 18, 'name': 'Rugby Union', 'type': 'SPORT'}, 28 | {'id': 120, 'name': 'Snooker', 'type': 'SPORT'}, 29 | {'id': 15, 'name': 'Soccer', 'type': 'SPORT'}, 30 | {'id': 9, 'name': 'Tennis', 'type': 'SPORT'}, 31 | {'id': 502491395980009, 'name': 'Test Sport', 'type': 'SPORT'}, 32 | {'id': 123, 'name': 'eSports', 'type': 'SPORT'}], 33 | 'total': 30} -------------------------------------------------------------------------------- /matchbook/resources/bettingresources.py: -------------------------------------------------------------------------------- 1 | from matchbook.resources.baseresource import BaseResource 2 | 3 | 4 | class MatchedBets(BaseResource): 5 | class Meta(BaseResource.Meta): 6 | identifier = 'matched-bets' 7 | attributes = { 8 | 'commission': 'commission', 9 | 'created-at': 'created-at', 10 | 'currency': 'currency', 11 | 'decimal-odds': 'decimal-odds', 12 | 'id': 'id', 13 | 'odds': 'odds', 14 | 'odds-type': 'odds-type', 15 | 'potential-profit': 'potential-profit', 16 | 'stake': 'stake', 17 | } 18 | datetime_attributes = ( 19 | 'created-at' 20 | ) 21 | 22 | 23 | class Order(BaseResource): 24 | class Meta(BaseResource.Meta): 25 | attributes = { 26 | 'created-at': 'created-at', 27 | 'currency': 'currency', 28 | 'decimal-odds': 'decimal-odds', 29 | 'event-id': 'event-id', 30 | 'event-name': 'event-name', 31 | 'exchange-type': 'exchange-type', 32 | 'id': 'id', 33 | 'market-id': 'market-id', 34 | 'market-name': 'market-name', 35 | 'odds': 'odds', 36 | 'odds-type': 'odds-type', 37 | 'potential-profit': 'potential-profit', 38 | 'remaining': 'remaining', 39 | 'remaining-potential-profit': 'remaining-potential-profit', 40 | 'runner-id': 'runner-id', 41 | 'runner-name': 'runner-name', 42 | 'side': 'side', 43 | 'stake': 'stake', 44 | 'status': 'status', 45 | } 46 | sub_resources = { 47 | 'matched-bets': MatchedBets, 48 | } 49 | datetime_attributes = ( 50 | 'created-at' 51 | ) 52 | 53 | 54 | class Position(BaseResource): 55 | class Meta(BaseResource.Meta): 56 | attributes = { 57 | 58 | } -------------------------------------------------------------------------------- /matchbook/baseclient.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | 4 | from matchbook.exceptions import PasswordError 5 | from matchbook.enums import ExchangeType, OddsType, Currency 6 | 7 | 8 | class BaseClient(object): 9 | 10 | def __init__(self, username, password=None, locale=None): 11 | """ 12 | :param username: Matchbook username. 13 | :param password: Password for supplied username, if None will look in for MATCHBOOK_PW in env variables. 14 | """ 15 | self.username = username 16 | self.password = password 17 | self.locale = locale 18 | self.url = 'https://www.matchbook.com' 19 | self.url_beta = 'https://beta.matchbook.com' 20 | self.urn_main = '/bpapi/rest/' 21 | self.urn_edge = '/edge/rest/' 22 | self.session = requests.Session() 23 | self.session_token = None 24 | self.user_id = None 25 | self.exchange_type = ExchangeType.BackLay 26 | self.odds_type = OddsType.Decimal 27 | self.currency = Currency.EUR 28 | self.get_password() 29 | 30 | def set_session_token(self, session_token, user_id): 31 | """Sets session token. 32 | 33 | :param session_token: Session token from request. 34 | :param user_id: User Id from the request. 35 | """ 36 | self.session_token = session_token 37 | self.user_id = user_id 38 | 39 | def get_password(self): 40 | """If password is not provided will look in environment 41 | variables for username+'password' 42 | """ 43 | if self.password is None: 44 | if os.environ.get('MATCHBOOK_PW'): 45 | self.password = os.environ.get('MATCHBOOK_PW') 46 | else: 47 | raise PasswordError() 48 | 49 | @property 50 | def headers(self): 51 | """Set headers to be used in API requests.""" 52 | return { 53 | 'Content-Type': 'application/json', 54 | 'Accept': 'application/json', 55 | } 56 | -------------------------------------------------------------------------------- /matchbook/endpoints/keepalive.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | 4 | from matchbook.endpoints.baseendpoint import BaseEndpoint 5 | from matchbook.exceptions import AuthError 6 | 7 | 8 | class KeepAlive(BaseEndpoint): 9 | 10 | def __call__(self, session=None): 11 | session = session or self.client.session 12 | response = self.request('GET', self.client.urn_main, 'security/session', data=self.data, session=session) 13 | if response.status_code == 200: 14 | pass 15 | elif response.status_code == 401: 16 | response = self.request("POST", self.client.urn_main, 'security/session', data=self.data, session=session) 17 | if response.status_code == 200: 18 | response_json = response.json() 19 | self.client.set_session_token(response_json.get('session-token'), response_json.get('user-id')) 20 | else: 21 | raise AuthError(response) 22 | else: 23 | raise AuthError(response) 24 | 25 | def request(self, request_method, urn, method, params={}, data={}, target=None, session=None): 26 | """ 27 | :param request_method: type of request to be sent. 28 | :param urn: matchbook urn to append to url specified. 29 | :param method: Matchbook method to be used. 30 | :param params: Params to be used in request 31 | :param url: define different URL to use. 32 | :param data: data to be sent in request body. 33 | :param target: target key to get from data, if None returns full response. 34 | :param session: Requests session to be used, reduces latency. 35 | """ 36 | session = session or self.client.session 37 | data['session-token'] = self.client.session_token 38 | data['user-id'] = self.client.user_id 39 | request_url = '%s%s%s' % (self.client.url, urn, method) 40 | response = session.request( 41 | request_method, request_url, params=params, data=json.dumps(data), headers=self.client.headers 42 | ) 43 | return response 44 | 45 | @property 46 | def data(self): 47 | return {'username': self.client.username, 'password': self.client.password} -------------------------------------------------------------------------------- /matchbook/tests/test_account.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import unittest.mock as mock 4 | 5 | from matchbook.apiclient import APIClient 6 | from matchbook.endpoints.account import Account 7 | 8 | 9 | class AccountTest(unittest.TestCase): 10 | 11 | def setUp(self): 12 | client = APIClient('username', 'password') 13 | self.account = Account(client) 14 | 15 | @mock.patch('matchbook.endpoints.account.Account.process_response') 16 | @mock.patch('matchbook.endpoints.account.Account.request', return_value=mock.Mock()) 17 | def test_get_account(self, mock_request, mock_process_response): 18 | self.account.get_account(balance_only=False) 19 | 20 | mock_request.assert_called_once_with("GET", '/edge/rest/', 'account', session=None) 21 | assert mock_process_response.call_count == 1 22 | 23 | @mock.patch('matchbook.endpoints.account.Account.process_response') 24 | @mock.patch('matchbook.endpoints.account.Account.request', return_value=mock.Mock()) 25 | def test_get_account_balance_only(self, mock_request, mock_process_response): 26 | self.account.get_account(balance_only=True) 27 | 28 | mock_request.assert_called_once_with("GET", '/edge/rest/', 'account/balance', session=None) 29 | assert mock_process_response.call_count == 1 30 | 31 | @mock.patch('matchbook.endpoints.account.Account.process_response') 32 | @mock.patch('matchbook.endpoints.account.Account.request', return_value=mock.Mock()) 33 | def test_wallet_transfer(self, mock_request, mock_process_response): 34 | self.account.wallet_transfer(amount=10.0) 35 | 36 | mock_request.assert_called_once_with( 37 | "POST", '/bpapi/rest/', 'account/transfer', data={'amount': 10.0}, session=None 38 | ) 39 | assert mock_process_response.call_count == 1 40 | 41 | @mock.patch('matchbook.endpoints.account.Account.process_response') 42 | @mock.patch('matchbook.endpoints.account.Account.request', return_value=mock.Mock()) 43 | def test_get_casino_balance(self, mock_request, mock_process_response): 44 | self.account.get_casino_balance() 45 | 46 | mock_request.assert_called_once_with("GET", '/bpapi/rest/', 'account/balance', session=None) 47 | assert mock_process_response.call_count == 1 48 | -------------------------------------------------------------------------------- /matchbook/tests/resources/betting_orders.json: -------------------------------------------------------------------------------- 1 | [{'acceptor-commission-rate': 0.0168, 2 | 'commission-reserve': 0.51534, 3 | 'commission-type': 'VOLUME', 4 | 'created-at': '2017-08-01T08:23:34.366Z', 5 | 'currency': 'EUR', 6 | 'decimal-odds': 1.6135, 7 | 'event-id': 553859672020032, 8 | 'event-name': 'C Berlocq vs G Melzer', 9 | 'exchange-type': 'back-lay', 10 | 'id': 556189429360015, 11 | 'in-play': False, 12 | 'market-id': 553859672230032, 13 | 'market-name': 'Moneyline', 14 | 'market-type': 'money_line', 15 | 'odds': 1.61349, 16 | 'odds-type': 'DECIMAL', 17 | 'offer-edits': [{'decimal-odds-after': 1.60976, 18 | 'decimal-odds-before': 1.60607, 19 | 'edit-time': '2017-08-01T08:24:00.623Z', 20 | 'id': 556189691930115, 21 | 'odds-after': 1.60975, 22 | 'odds-before': 1.60606, 23 | 'odds-type': 'DECIMAL', 24 | 'offer-id': 556189429360015, 25 | 'runner-id': 553859672320109, 26 | 'stake-after': 50, 27 | 'stake-before': 50}, 28 | {'decimal-odds-after': 1.60607, 29 | 'decimal-odds-before': 1.60976, 30 | 'edit-time': '2017-08-01T08:24:06.676Z', 31 | 'id': 556189752460115, 32 | 'odds-after': 1.60606, 33 | 'odds-before': 1.60975, 34 | 'odds-type': 'DECIMAL', 35 | 'offer-id': 556189429360015, 36 | 'runner-id': 553859672320109, 37 | 'stake-after': 50, 38 | 'stake-before': 50}, 39 | {'decimal-odds-after': 1.60976, 40 | 'decimal-odds-before': 1.60607, 41 | 'edit-time': '2017-08-01T08:24:22.963Z', 42 | 'id': 556189915330115, 43 | 'odds-after': 1.60975, 44 | 'odds-before': 1.60606, 45 | 'odds-type': 'DECIMAL', 46 | 'offer-id': 556189429360015, 47 | 'runner-id': 553859672320109, 48 | 'stake-after': 50, 49 | 'stake-before': 50}, 50 | {'decimal-odds-after': 1.6135, 51 | 'decimal-odds-before': 1.60976, 52 | 'edit-time': '2017-08-01T08:24:30.223Z', 53 | 'id': 556189987930115, 54 | 'odds-after': 1.61349, 55 | 'odds-before': 1.60975, 56 | 'odds-type': 'DECIMAL', 57 | 'offer-id': 556189429360015, 58 | 'runner-id': 553859672320109, 59 | 'stake-after': 50, 60 | 'stake-before': 50}], 61 | 'originator-commission-rate': 0.0084, 62 | 'potential-liability': 30.67485, 63 | 'remaining': 50, 64 | 'remaining-potential-liability': 30.67485, 65 | 'runner-id': 553859672320109, 66 | 'runner-name': 'C Berlocq', 67 | 'side': 'lay', 68 | 'sport-id': 9, 69 | 'stake': 50, 70 | 'status': 'edited', 71 | 'temp-id': '168'}] -------------------------------------------------------------------------------- /matchbook/tests/resources/betting_delete_bulk.json: -------------------------------------------------------------------------------- 1 | {'currency': 'EUR', 2 | 'exchange-type': 'back-lay', 3 | 'language': 'en', 4 | 'odds-type': 'DECIMAL', 5 | 'offers': [{'acceptor-commission-rate': 0.0168, 6 | 'commission-reserve': 0.51534, 7 | 'commission-type': 'VOLUME', 8 | 'created-at': '2017-08-01T08:23:34.366Z', 9 | 'currency': 'EUR', 10 | 'decimal-odds': 1.61349, 11 | 'event-id': 553859672020032, 12 | 'event-name': 'C Berlocq vs G Melzer', 13 | 'exchange-type': 'back-lay', 14 | 'id': 556189429360015, 15 | 'in-play': False, 16 | 'market-id': 553859672230032, 17 | 'market-name': 'Moneyline', 18 | 'market-type': 'money_line', 19 | 'odds': 1.61349, 20 | 'odds-type': 'DECIMAL', 21 | 'offer-edits': [{'decimal-odds-after': 1.60976, 22 | 'decimal-odds-before': 1.60607, 23 | 'edit-time': '2017-08-01T08:24:00.623Z', 24 | 'id': 556189691930115, 25 | 'odds-after': 1.60975, 26 | 'odds-before': 1.60606, 27 | 'odds-type': 'DECIMAL', 28 | 'offer-id': 556189429360015, 29 | 'stake-after': 50, 30 | 'stake-before': 50}, 31 | {'decimal-odds-after': 1.60607, 32 | 'decimal-odds-before': 1.60976, 33 | 'edit-time': '2017-08-01T08:24:06.676Z', 34 | 'id': 556189752460115, 35 | 'odds-after': 1.60606, 36 | 'odds-before': 1.60975, 37 | 'odds-type': 'DECIMAL', 38 | 'offer-id': 556189429360015, 39 | 'stake-after': 50, 40 | 'stake-before': 50}, 41 | {'decimal-odds-after': 1.60976, 42 | 'decimal-odds-before': 1.60607, 43 | 'edit-time': '2017-08-01T08:24:22.963Z', 44 | 'id': 556189915330115, 45 | 'odds-after': 1.60975, 46 | 'odds-before': 1.60606, 47 | 'odds-type': 'DECIMAL', 48 | 'offer-id': 556189429360015, 49 | 'stake-after': 50, 50 | 'stake-before': 50}, 51 | {'decimal-odds-after': 1.6135, 52 | 'decimal-odds-before': 1.60976, 53 | 'edit-time': '2017-08-01T08:24:30.223Z', 54 | 'id': 556189987930115, 55 | 'odds-after': 1.61349, 56 | 'odds-before': 1.60975, 57 | 'odds-type': 'DECIMAL', 58 | 'offer-id': 556189429360015, 59 | 'stake-after': 50, 60 | 'stake-before': 50}], 61 | 'originator-commission-rate': 0.0084, 62 | 'potential-liability': 30.67485, 63 | 'remaining': 50, 64 | 'remaining-potential-liability': 30.67485, 65 | 'runner-id': 553859672320109, 66 | 'runner-name': 'C Berlocq', 67 | 'side': 'lay', 68 | 'stake': 50, 69 | 'status': 'cancelled', 70 | 'temp-id': '168'}]} -------------------------------------------------------------------------------- /matchbook/endpoints/account.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from matchbook.endpoints.baseendpoint import BaseEndpoint 4 | from matchbook import resources 5 | from matchbook.utils import clean_locals 6 | 7 | 8 | class Account(BaseEndpoint): 9 | 10 | def get_account(self, balance_only=True, session=None): 11 | """ 12 | Get account information for logged in user. 13 | 14 | :param balance_only: retrieve only account balance info subset or not. 15 | :type balance_only: bool 16 | :param session: requests session to be used. 17 | :type session: requests.Session 18 | :returns: Returns the account details for the logged-in user. 19 | :rtype: json 20 | :raises: MatchbookAPI.bin.exceptions.ApiError 21 | 22 | """ 23 | method = 'account' 24 | resource = resources.AccountDetails 25 | if balance_only: 26 | method = 'account/balance' 27 | resource = resources.AccountBalance 28 | date_time_sent = datetime.datetime.utcnow() 29 | response = self.request("GET", self.client.urn_edge, method, session=session) 30 | date_time_received = datetime.datetime.utcnow() 31 | return self.process_response(response.json(), resource, date_time_sent, date_time_received) 32 | 33 | def wallet_transfer(self, amount, session=None): 34 | #TODO: Populate Acccount Transfer Resource. 35 | """ 36 | Transfer balance from one 37 | 38 | :param amount: amount to be transferred, >0 for casino to sports transfer, <0 for the opposite. 39 | :type amount: float 40 | :param session: requests session to be used. 41 | :type session: requests.Session 42 | :return: details of the success/failure of the transfer. 43 | """ 44 | params = clean_locals(locals()) 45 | date_time_sent = datetime.datetime.utcnow() 46 | date_time_received = datetime.datetime.utcnow() 47 | response = self.request("POST", self.client.urn_main, 'account/transfer', data=params, session=session) 48 | return self.process_response(response.json(), resources.AccountTransfer, date_time_sent, date_time_received) 49 | 50 | def get_casino_balance(self, session=None): 51 | """ 52 | Get casino account balance for logged in user. 53 | 54 | :param session: requests session to be used. 55 | :type session: requests.Session 56 | :returns: Returns the casino balance for the logged-in user. 57 | :rtype: json 58 | :raises: MatchbookAPI.bin.exceptions.ApiError 59 | 60 | """ 61 | date_time_sent = datetime.datetime.utcnow() 62 | response = self.request("GET", self.client.urn_main, 'account/balance', session=session) 63 | date_time_received = datetime.datetime.utcnow() 64 | return self.process_response(response.json(), resources.AccountBalance, date_time_sent, date_time_received) 65 | -------------------------------------------------------------------------------- /matchbook/tests/test_mbapi.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | from unittest import TestCase 4 | from matchbook.apiclient import APIClient 5 | 6 | 7 | class TestMatchbook(TestCase): 8 | api = APIClient( 9 | username=os.environ['MATCHBOOK_USERNAME'], 10 | password=os.environ['MATCHBOOK_PW'] 11 | ) 12 | api.login() 13 | 14 | def test_settled(self): 15 | settled = self.api.reporting.get_settlement_bets() 16 | print(settled) 17 | 18 | def test_navigation(self): 19 | nav = self.api.reference_data.get_navigation() 20 | assert nav 21 | 22 | def test_send_order(self): 23 | # self.api.send_orders( 24 | # runner_id=432346050920009, 25 | # odds=3.5, 26 | # side='BACK', 27 | # stake=10, 28 | # temp_id='' 29 | # ) 30 | return 31 | 32 | def test_get_prices(self): 33 | tennis_markets = self.api.market_data.get_events(sport_ids=[9]) 34 | top_event = sorted(tennis_markets, key=lambda x: x['volume'], reverse=True)[0] 35 | event_id = top_event['id'] 36 | market_id = next(mkt['id'] for mkt in top_event['markets'] if mkt['market-type'] == 'money_line') 37 | self.api.market_data.get_markets(event_id, market_id) 38 | 39 | def test_get_sports(self): 40 | sports = self.api.reference_data.get_sports() 41 | assert sports 42 | 43 | def test_send_quotes(self): 44 | order_list = [ 45 | {'market_id': '450147584000010', 'order_type': 'GFD', 'timestamp': 1491008502, 'credit': 0, 'channel_type': 'matchbook', 46 | 'side': 'back', 'size': 55.0, 'runner_id': 450147584080010, 'runner': 'Utah Jazz', 'price': 1.81}, 47 | {'market_id': '450147584000010', 'order_type': 'GFD', 'timestamp': 1491008502, 'credit': 0, 'channel_type': 'matchbook', 48 | 'side': 'lay', 'size': 60.0, 'runner_id': 450147584080010, 'runner': 'Utah Jazz', 'price': 1.66}, 49 | {'market_id': '450147584000010', 'order_type': 'GFD', 'timestamp': 1491008502, 'credit': 0, 'channel_type': 'matchbook', 50 | 'side': 'back', 'size': 40.0, 'runner_id': 450147584030010, 'runner': 'Washington Wizards', 'price': 2.48}, 51 | {'market_id': '450147584000010', 'order_type': 'GFD', 'timestamp': 1491008502, 'credit': 0, 'channel_type': 'matchbook', 52 | 'side': 'lay', 'size': 7.0, 'runner_id': 450147584030010, 'runner': 'Washington Wizards', 'price': 2.22} 53 | ] 54 | 55 | orders_transposed = dict(zip(order_list[0], zip(*[d.values() for d in order_list]))) 56 | 57 | resp = self.api.betting.send_orders( 58 | runner_id=list(orders_transposed.get('runner_id', ())), 59 | odds=list(orders_transposed.get('price', ())), 60 | side=list(orders_transposed.get('side', ())), 61 | stake=list(orders_transposed.get('size', ())), 62 | temp_id=orders_transposed.get('customer_reference'), 63 | ) 64 | -------------------------------------------------------------------------------- /matchbook/resources/marketdataresources.py: -------------------------------------------------------------------------------- 1 | from matchbook.resources import BaseResource 2 | 3 | 4 | class Price(BaseResource): 5 | class Meta(BaseResource.Meta): 6 | identifier = 'prices' 7 | attributes = { 8 | 'available-amount': 'available-amount', 9 | 'currency': 'currency', 10 | 'decimal-odds': 'decimal-odds', 11 | 'exchange-type': 'exchange-type', 12 | 'odds': 'odds', 13 | 'odds-type': 'odds-type', 14 | 'side': 'side', 15 | } 16 | 17 | 18 | class Runner(BaseResource): 19 | class Meta(BaseResource.Meta): 20 | identifier = 'runners' 21 | attributes = { 22 | 'event-id': 'event-id', 23 | 'event-participant-id': 'event-participant-id', 24 | 'id': 'id', 25 | 'market-id': 'market-id', 26 | 'name': 'name', 27 | 'status': 'status', 28 | 'volume': 'volume', 29 | 'withdrawn': 'withdrawn', 30 | } 31 | sub_resources = { 32 | 'prices': Price, 33 | } 34 | 35 | 36 | class Market(BaseResource): 37 | class Meta(BaseResource.Meta): 38 | identifier = 'markets' 39 | attributes = { 40 | 'allow-live-betting': 'allow-live-betting', 41 | 'back-overround': 'back-overround', 42 | 'event-id': 'event-id', 43 | 'grading-type': 'grading-type', 44 | 'handicap': 'handicap', 45 | 'id': 'id', 46 | 'in-running-flag': 'in-running-flag', 47 | 'lay-overround': 'lay-overround', 48 | 'market-ids': 'market-ids', 49 | 'name': 'name', 50 | 'runner-ids': 'runner-ids', 51 | 'start': 'start', 52 | 'status': 'status', 53 | 'type': 'type', 54 | 'market-type': 'market-type', 55 | 'volume': 'volume', 56 | } 57 | sub_resources = { 58 | 'runners': Runner, 59 | } 60 | datetime_attributes = ( 61 | 'start' 62 | ) 63 | 64 | 65 | class EventMeta(BaseResource): 66 | class Meta(BaseResource.Meta): 67 | identifier = 'meta-tags' 68 | attributes = { 69 | 'id': 'id', 70 | 'name': 'name', 71 | 'type': 'type', 72 | 'url-name': 'url-name', 73 | } 74 | 75 | 76 | class Event(BaseResource): 77 | class Meta(BaseResource.Meta): 78 | attributes = { 79 | 'allow-live-betting': 'allow-live-betting', 80 | 'category-id': 'category-id', 81 | 'id': 'id', 82 | 'in-running-flag': 'in-running-flag', 83 | 'name': 'name', 84 | 'sport-id': 'sport-id', 85 | 'start': 'start', 86 | 'status': 'status', 87 | 'volume': 'volume', 88 | } 89 | sub_resources = { 90 | 'markets': Market, 91 | 'meta-tags': EventMeta, 92 | } 93 | datetime_attributes = ( 94 | 'start' 95 | ) 96 | -------------------------------------------------------------------------------- /matchbook/tests/resources/marketdata_runners.json: -------------------------------------------------------------------------------- 1 | {'runners': [{'event-id': 553823925620032, 2 | 'event-participant-id': 553823925680009, 3 | 'id': 553823925840032, 4 | 'market-id': 553823925780009, 5 | 'name': 'T Paul', 6 | 'prices': [{'available-amount': 101.53652, 7 | 'currency': 'EUR', 8 | 'decimal-odds': 2.49, 9 | 'exchange-type': 'back-lay', 10 | 'odds': 2.49, 11 | 'odds-type': 'DECIMAL', 12 | 'side': 'back'}, 13 | {'available-amount': 60.92369, 14 | 'currency': 'EUR', 15 | 'decimal-odds': 2.43, 16 | 'exchange-type': 'back-lay', 17 | 'odds': 2.43, 18 | 'odds-type': 'DECIMAL', 19 | 'side': 'back'}, 20 | {'available-amount': 81.22924, 21 | 'currency': 'EUR', 22 | 'decimal-odds': 2.22, 23 | 'exchange-type': 'back-lay', 24 | 'odds': 2.22, 25 | 'odds-type': 'DECIMAL', 26 | 'side': 'back'}, 27 | {'available-amount': 1573.45549, 28 | 'currency': 'EUR', 29 | 'decimal-odds': 2.6, 30 | 'exchange-type': 'back-lay', 31 | 'odds': 2.6, 32 | 'odds-type': 'DECIMAL', 33 | 'side': 'lay'}, 34 | {'available-amount': 909.94976, 35 | 'currency': 'EUR', 36 | 'decimal-odds': 2.66, 37 | 'exchange-type': 'back-lay', 38 | 'odds': 2.66, 39 | 'odds-type': 'DECIMAL', 40 | 'side': 'lay'}, 41 | {'available-amount': 1017.18454, 42 | 'currency': 'EUR', 43 | 'decimal-odds': 2.98, 44 | 'exchange-type': 'back-lay', 45 | 'odds': 2.98, 46 | 'odds-type': 'DECIMAL', 47 | 'side': 'lay'}], 48 | 'status': 'open', 49 | 'volume': 28298.69329}, 50 | {'event-id': 553823925620032, 51 | 'event-participant-id': 553823925690032, 52 | 'id': 553823925840009, 53 | 'market-id': 553823925780009, 54 | 'name': 'C Ruud', 55 | 'prices': [{'available-amount': 2517.52879, 56 | 'currency': 'EUR', 57 | 'decimal-odds': 1.625, 58 | 'exchange-type': 'back-lay', 59 | 'odds': 1.625, 60 | 'odds-type': 'DECIMAL', 61 | 'side': 'back'}, 62 | {'available-amount': 1510.5166, 63 | 'currency': 'EUR', 64 | 'decimal-odds': 1.6024, 65 | 'exchange-type': 'back-lay', 66 | 'odds': 1.6024, 67 | 'odds-type': 'DECIMAL', 68 | 'side': 'back'}, 69 | {'available-amount': 2014.0254, 70 | 'currency': 'EUR', 71 | 'decimal-odds': 1.50505, 72 | 'exchange-type': 'back-lay', 73 | 'odds': 1.50505, 74 | 'odds-type': 'DECIMAL', 75 | 'side': 'back'}, 76 | {'available-amount': 151.28941, 77 | 'currency': 'EUR', 78 | 'decimal-odds': 1.67115, 79 | 'exchange-type': 'back-lay', 80 | 'odds': 1.67115, 81 | 'odds-type': 'DECIMAL', 82 | 'side': 'lay'}, 83 | {'available-amount': 87.12087, 84 | 'currency': 'EUR', 85 | 'decimal-odds': 1.69931, 86 | 'exchange-type': 'back-lay', 87 | 'odds': 1.69931, 88 | 'odds-type': 'DECIMAL', 89 | 'side': 'lay'}, 90 | {'available-amount': 99.09968, 91 | 'currency': 'EUR', 92 | 'decimal-odds': 1.81968, 93 | 'exchange-type': 'back-lay', 94 | 'odds': 1.81968, 95 | 'odds-type': 'DECIMAL', 96 | 'side': 'lay'}], 97 | 'status': 'open', 98 | 'volume': 25156.41754}], 99 | 'total': 2} -------------------------------------------------------------------------------- /matchbook/endpoints/baseendpoint.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from matchbook.utils import check_status_code, check_call_complete 4 | 5 | 6 | class BaseEndpoint(object): 7 | 8 | def __init__(self, parent): 9 | """ 10 | :param parent: API client. 11 | """ 12 | self.client = parent 13 | 14 | def request(self, request_method, urn, method, params={}, data={}, target=None, session=None): 15 | """ 16 | :param request_method: type of request to be sent. 17 | :param urn: matchbook urn to append to url specified. 18 | :param method: Matchbook method to be used. 19 | :param params: Params to be used in request. 20 | :param data: data to be sent in request body. 21 | :param target: target to get from returned data, if none returns full response. 22 | :param session: Requests session to be used, reduces latency. 23 | """ 24 | session = session or self.client.session 25 | data['session-token'] = self.client.session_token 26 | data['user-id'] = self.client.user_id 27 | request_url = '%s%s%s' % (self.client.url, urn, method) 28 | response = session.request( 29 | request_method, request_url, params=params, data=json.dumps(data), headers=self.client.headers 30 | ) 31 | check_status_code(response) 32 | if ('per-page' in params.keys()) and target: 33 | resp_data = response.json().get(target, []) 34 | while not check_call_complete(response.json()): 35 | params['offset'] += response.json().get('total', 0) + 1 36 | response = session.request( 37 | request_method, request_url, params=params, data=json.dumps(data), headers=self.client.headers 38 | ) 39 | resp_data += response.json().get(target, []) 40 | return resp_data 41 | else: 42 | return response 43 | 44 | @staticmethod 45 | def process_response(response_json, resource, date_time_sent, date_time_received=None): 46 | """ 47 | :param response_json: Response in json format 48 | :param resource: Resource data structure 49 | :param date_time_sent: Date time sent 50 | :param date_time_received: Date time received response from request 51 | """ 52 | if isinstance(response_json, list): 53 | return [ 54 | resource(date_time_sent=date_time_sent, TIMESTAMP=date_time_received.strftime('%Y-%m-%d %H:%M:%S.%f'), 55 | **x).json() for x in response_json] 56 | else: 57 | response_result = response_json.get('result', response_json) 58 | if isinstance(response_result, list): 59 | return [resource(date_time_sent=date_time_sent, 60 | TIMESTAMP=date_time_received.strftime('%Y-%m-%d %H:%M:%S.%f'), 61 | **x).json() for x in response_result] 62 | else: 63 | return resource(date_time_sent=date_time_sent, 64 | TIMESTAMP=date_time_received.strftime('%Y-%m-%d %H:%M:%S.%f'), 65 | **response_result).json() 66 | -------------------------------------------------------------------------------- /matchbook/tests/test_referencedata.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import unittest.mock as mock 4 | 5 | from matchbook.apiclient import APIClient 6 | from matchbook.endpoints.referencedata import ReferenceData 7 | 8 | 9 | class ReferenceDataTest(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.client = APIClient('username', 'password') 13 | self.reference_data = ReferenceData(self.client) 14 | 15 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.process_response') 16 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.request', return_value=mock.Mock()) 17 | def test_get_currencies(self, mock_request, mock_process_response): 18 | self.reference_data.get_currencies() 19 | 20 | mock_request.assert_called_once_with("GET", self.client.urn_main, 'lookups/currencies', session=None,) 21 | assert mock_process_response.call_count == 1 22 | 23 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.process_response') 24 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.request', return_value=mock.Mock()) 25 | def test_get_sports(self, mock_request, mock_process_response): 26 | self.reference_data.get_sports() 27 | 28 | mock_request.assert_called_once_with("GET", self.client.urn_edge, 'lookups/sports', 29 | params={'order': 'name asc', 'per-page': 500}, session=None,) 30 | assert mock_process_response.call_count == 1 31 | 32 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.process_response') 33 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.request', return_value=mock.Mock()) 34 | def test_get_oddstype(self, mock_request, mock_process_response): 35 | self.reference_data.get_oddstype() 36 | 37 | mock_request.assert_called_once_with("GET", self.client.urn_main, 'lookups/odds-types', session=None,) 38 | assert mock_process_response.call_count == 1 39 | 40 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.process_response') 41 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.request', return_value=mock.Mock()) 42 | def test_get_countries(self, mock_request, mock_process_response): 43 | self.reference_data.get_countries() 44 | 45 | mock_request.assert_called_once_with("GET", self.client.urn_main, 'lookups/countries', session=None,) 46 | assert mock_process_response.call_count == 1 47 | 48 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.process_response') 49 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.request', return_value=mock.Mock()) 50 | def test_get_regions(self, mock_request, mock_process_response): 51 | self.reference_data.get_regions(country_id=1) 52 | 53 | mock_request.assert_called_once_with("GET", self.client.urn_main, 'lookups/regions/1', session=None,) 54 | assert mock_process_response.call_count == 1 55 | 56 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.process_response') 57 | @mock.patch('matchbook.endpoints.referencedata.ReferenceData.request', return_value=mock.Mock()) 58 | def test_get_navigation(self, mock_request, mock_process_response): 59 | self.reference_data.get_navigation() 60 | 61 | mock_request.assert_called_once_with( 62 | "GET", self.client.urn_edge, 'navigation', params={'offset': 0, 'per-page': 500}, session=None, 63 | ) 64 | assert mock_process_response.call_count == 1 65 | -------------------------------------------------------------------------------- /matchbook/tests/test_reporting.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import unittest.mock as mock 4 | 5 | from matchbook.apiclient import APIClient 6 | from matchbook.endpoints.reporting import Reporting 7 | from matchbook.enums import TransactionCategories, TransactionTypes 8 | 9 | 10 | class ReportingTest(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.client = APIClient('username', 'password') 14 | self.reporting = Reporting(self.client) 15 | 16 | @mock.patch('matchbook.endpoints.reporting.Reporting.process_response') 17 | @mock.patch('matchbook.endpoints.reporting.Reporting.request', return_value=mock.Mock()) 18 | def test_get_old_transactions_report(self, mock_request, mock_process_response): 19 | self.reporting.get_old_transactions_report(offset=0, per_page=500, categories=TransactionCategories.Exchange) 20 | 21 | mock_request.assert_called_once_with( 22 | "GET", self.client.urn_main, 'reports/transactions', target='transactions', session=None, 23 | params={'offset': 0, 'per-page': 500, 'categories': 'exchange'}, 24 | ) 25 | assert mock_process_response.call_count == 1 26 | 27 | @mock.patch('matchbook.endpoints.reporting.Reporting.process_response') 28 | @mock.patch('matchbook.endpoints.reporting.Reporting.request', return_value=mock.Mock()) 29 | def test_get_new_transactions_report(self, mock_request, mock_process_response): 30 | self.reporting.get_new_transactions_report(transaction_type=TransactionTypes.All, offset=0, per_page=500) 31 | 32 | mock_request.assert_called_once_with("GET", self.client.urn_edge, 'reports/v1/transactions', 33 | target='transactions', session=None, params={'offset': 0, 'per-page': 500}) 34 | assert mock_process_response.call_count == 1 35 | 36 | @mock.patch('matchbook.endpoints.reporting.Reporting.process_response') 37 | @mock.patch('matchbook.endpoints.reporting.Reporting.request', return_value=mock.Mock()) 38 | def test_get_current_offers(self, mock_request, mock_process_response): 39 | self.reporting.get_current_offers(sport_ids='9', offset=0, per_page=500) 40 | 41 | mock_request.assert_called_once_with( 42 | "GET", self.client.urn_edge, 'reports/v1/offers/current', target='offers', session=None, 43 | params={'sport-ids': '9', 'offset': 0, 'per-page': 500}) 44 | assert mock_process_response.call_count == 1 45 | 46 | @mock.patch('matchbook.endpoints.reporting.Reporting.process_response') 47 | @mock.patch('matchbook.endpoints.reporting.Reporting.request', return_value=mock.Mock()) 48 | def test_get_current_bets(self, mock_request, mock_process_response): 49 | self.reporting.get_current_bets(sport_ids='9', offset=0, per_page=500) 50 | 51 | mock_request.assert_called_once_with( 52 | "GET", self.client.urn_edge, 'reports/v1/bets/current', target='bets', session=None, 53 | params={'sport-ids': '9', 'offset': 0, 'per-page': 500}) 54 | assert mock_process_response.call_count == 1 55 | 56 | @mock.patch('matchbook.endpoints.reporting.Reporting.process_response') 57 | @mock.patch('matchbook.endpoints.reporting.Reporting.request', return_value=mock.Mock()) 58 | def test_get_settled_bets(self, mock_request, mock_process_response): 59 | self.reporting.get_settled_bets(sport_ids='9', offset=0, per_page=500) 60 | 61 | mock_request.assert_called_once_with( 62 | "GET", self.client.urn_edge, 'reports/v1/bets/settled', target='bets', session=None, 63 | params={'sport-ids': '9', 'offset': 0, 'per-page': 500}) 64 | assert mock_process_response.call_count == 1 65 | -------------------------------------------------------------------------------- /matchbook/tests/resources/marketdata_markets.json: -------------------------------------------------------------------------------- 1 | [{'allow-live-betting': True, 2 | 'back-overround': 101.526, 3 | 'event-id': 553823925620032, 4 | 'id': 553823925780009, 5 | 'in-running-flag': True, 6 | 'lay-overround': 98.474, 7 | 'live': True, 8 | 'market-type': 'money_line', 9 | 'name': 'Moneyline', 10 | 'runners': [{'event-id': 553823925620032, 11 | 'event-participant-id': 553823925680009, 12 | 'id': 553823925840032, 13 | 'market-id': 553823925780009, 14 | 'name': 'T Paul', 15 | 'prices': [{'available-amount': 71.92688, 16 | 'currency': 'EUR', 17 | 'decimal-odds': 2.25, 18 | 'exchange-type': 'back-lay', 19 | 'odds': 2.25, 20 | 'odds-type': 'DECIMAL', 21 | 'side': 'back'}, 22 | {'available-amount': 43.15295, 23 | 'currency': 'EUR', 24 | 'decimal-odds': 2.21, 25 | 'exchange-type': 'back-lay', 26 | 'odds': 2.21, 27 | 'odds-type': 'DECIMAL', 28 | 'side': 'back'}, 29 | {'available-amount': 217.25146, 30 | 'currency': 'EUR', 31 | 'decimal-odds': 2.12, 32 | 'exchange-type': 'back-lay', 33 | 'odds': 2.12, 34 | 'odds-type': 'DECIMAL', 35 | 'side': 'back'}, 36 | {'available-amount': 216.26229, 37 | 'currency': 'EUR', 38 | 'decimal-odds': 2.33, 39 | 'exchange-type': 'back-lay', 40 | 'odds': 2.33, 41 | 'odds-type': 'DECIMAL', 42 | 'side': 'lay'}, 43 | {'available-amount': 457.41626, 44 | 'currency': 'EUR', 45 | 'decimal-odds': 2.36, 46 | 'exchange-type': 'back-lay', 47 | 'odds': 2.36, 48 | 'odds-type': 'DECIMAL', 49 | 'side': 'lay'}, 50 | {'available-amount': 264.71634, 51 | 'currency': 'EUR', 52 | 'decimal-odds': 2.41, 53 | 'exchange-type': 'back-lay', 54 | 'odds': 2.41, 55 | 'odds-type': 'DECIMAL', 56 | 'side': 'lay'}], 57 | 'status': 'open', 58 | 'volume': 28165.69329}, 59 | {'event-id': 553823925620032, 60 | 'event-participant-id': 553823925690032, 61 | 'id': 553823925840009, 62 | 'market-id': 553823925780009, 63 | 'name': 'C Ruud', 64 | 'prices': [{'available-amount': 287.62885, 65 | 'currency': 'EUR', 66 | 'decimal-odds': 1.75187, 67 | 'exchange-type': 'back-lay', 68 | 'odds': 1.75187, 69 | 'odds-type': 'DECIMAL', 70 | 'side': 'back'}, 71 | {'available-amount': 622.08611, 72 | 'currency': 'EUR', 73 | 'decimal-odds': 1.73529, 74 | 'exchange-type': 'back-lay', 75 | 'odds': 1.73529, 76 | 'odds-type': 'DECIMAL', 77 | 'side': 'back'}, 78 | {'available-amount': 373.25004, 79 | 'currency': 'EUR', 80 | 'decimal-odds': 1.70921, 81 | 'exchange-type': 'back-lay', 82 | 'odds': 1.70921, 83 | 'odds-type': 'DECIMAL', 84 | 'side': 'back'}, 85 | {'available-amount': 89.9086, 86 | 'currency': 'EUR', 87 | 'decimal-odds': 1.8, 88 | 'exchange-type': 'back-lay', 89 | 'odds': 1.8, 90 | 'odds-type': 'DECIMAL', 91 | 'side': 'lay'}, 92 | {'available-amount': 52.21508, 93 | 'currency': 'EUR', 94 | 'decimal-odds': 1.82645, 95 | 'exchange-type': 'back-lay', 96 | 'odds': 1.82645, 97 | 'odds-type': 'DECIMAL', 98 | 'side': 'lay'}, 99 | {'available-amount': 243.32163, 100 | 'currency': 'EUR', 101 | 'decimal-odds': 1.89286, 102 | 'exchange-type': 'back-lay', 103 | 'odds': 1.89286, 104 | 'odds-type': 'DECIMAL', 105 | 'side': 'lay'}], 106 | 'status': 'open', 107 | 'volume': 24960.48197}], 108 | 'start': '2017-07-31T20:09:00.000Z', 109 | 'status': 'open', 110 | 'type': 'binary', 111 | 'volume': 53126.17526}] -------------------------------------------------------------------------------- /matchbook/resources/baseresource.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | from ..compat import basestring, integer_types 5 | 6 | 7 | class BaseResource(object): 8 | """ 9 | Data structure based on a becket resource 10 | https://github.com/phalt/beckett 11 | """ 12 | 13 | class Meta: 14 | identifier = 'id' # The key with which you uniquely identify this resource. 15 | attributes = {'id': 'id'} # Acceptable attributes that you want to display in this resource. 16 | sub_resources = {} # sub resources are complex attributes within a resource 17 | datetime_attributes = () # Attributes to be converted to datetime 18 | 19 | def __init__(self, **kwargs): 20 | self._datetime_sent = kwargs.pop('date_time_sent', None) 21 | self.streaming_unique_id = kwargs.pop('streaming_unique_id', None) 22 | self.publish_time = kwargs.pop('publish_time', None) 23 | 24 | now = datetime.datetime.utcnow() 25 | self.datetime_created = now 26 | self._datetime_updated = now 27 | self._sub_resource_map = getattr(self.Meta, 'sub_resources', {}) 28 | self._data = kwargs 29 | self.set_attributes(**kwargs) 30 | self._data['Latency'] = self.elapsed_time 31 | 32 | def set_sub_resources(self, **kwargs): 33 | """ 34 | For each sub resource assigned to this resource, generate the 35 | sub resource instance and set it as an attribute on this instance. 36 | """ 37 | for attribute_name, resource in self._sub_resource_map.items(): 38 | sub_attr = kwargs.get(attribute_name) 39 | if sub_attr: 40 | if isinstance(sub_attr, list): 41 | value = [resource(**x) for x in sub_attr] # A list of sub resources is supported 42 | else: 43 | value = resource(**sub_attr) # So is a single resource 44 | setattr(self, resource.Meta.identifier, value) 45 | else: 46 | setattr(self, resource.Meta.identifier, []) # [] = Empty resource 47 | 48 | def set_attributes(self, **kwargs): 49 | """ 50 | Set the resource attributes from the kwargs. 51 | Only sets items in the `self.Meta.attributes` white list. 52 | Subclass this method to customise attributes. 53 | """ 54 | if self._sub_resource_map: 55 | self.set_sub_resources(**kwargs) 56 | for key in self._sub_resource_map.keys(): 57 | kwargs.pop(key, None) # Don't let these attributes be overridden later 58 | for field, value in kwargs.items(): 59 | if field in self.Meta.attributes: 60 | if field in self.Meta.datetime_attributes: 61 | value = self.strip_datetime(value) or value 62 | setattr(self, self.Meta.attributes[field], value) 63 | 64 | def json(self): 65 | return self._data 66 | 67 | def message(self): 68 | return json.dumps(self._data) 69 | 70 | @staticmethod 71 | def strip_datetime(value): 72 | """ 73 | Converts value to datetime if string or int. 74 | """ 75 | if isinstance(value, basestring): 76 | try: 77 | return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") 78 | except ValueError: 79 | return 80 | elif isinstance(value, integer_types): 81 | try: 82 | return datetime.datetime.utcfromtimestamp(value / 1e3) 83 | except (ValueError, OverflowError, OSError): 84 | return 85 | 86 | @property 87 | def elapsed_time(self): 88 | """ 89 | Elapsed time between datetime sent and datetime created 90 | """ 91 | if self._datetime_sent: 92 | return (self.datetime_created-self._datetime_sent).total_seconds() 93 | 94 | def __getattr__(self, item): 95 | """ 96 | If item is an expected attribute in Meta 97 | return None, if not raise Attribute error. 98 | """ 99 | if item in self.Meta.attributes.values(): 100 | return 101 | else: 102 | return self.__getattribute__(item) 103 | 104 | def __repr__(self): 105 | return '<%s>' % self.__class__.__name__ 106 | 107 | def __str__(self): 108 | return self.__class__.__name__ 109 | -------------------------------------------------------------------------------- /examples/matchbook_api_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 14, 6 | "metadata": { 7 | "collapsed": true, 8 | "deletable": true, 9 | "editable": true 10 | }, 11 | "outputs": [], 12 | "source": [ 13 | "import os\n", 14 | "from matchbook import APIClient\n", 15 | "from matchbook.enums import Side" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 2, 21 | "metadata": { 22 | "collapsed": false, 23 | "deletable": true, 24 | "editable": true 25 | }, 26 | "outputs": [], 27 | "source": [ 28 | "mb = APIClient('username', 'password')\n", 29 | "mb.login()" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 3, 35 | "metadata": { 36 | "collapsed": false, 37 | "deletable": true, 38 | "editable": true 39 | }, 40 | "outputs": [], 41 | "source": [ 42 | "sports = mb.reference_data.get_sports()" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "metadata": { 49 | "collapsed": false, 50 | "deletable": true, 51 | "editable": true 52 | }, 53 | "outputs": [], 54 | "source": [ 55 | "tennis_id = [s['id'] for s in sports if s['name']=='Tennis'][0]" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 5, 61 | "metadata": { 62 | "collapsed": false, 63 | "deletable": true, 64 | "editable": true 65 | }, 66 | "outputs": [], 67 | "source": [ 68 | "tennis_events = mb.market_data.get_events(sport_ids=tennis_id, include_event_participants='false')" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 6, 74 | "metadata": { 75 | "collapsed": false, 76 | "deletable": true, 77 | "editable": true 78 | }, 79 | "outputs": [], 80 | "source": [ 81 | "specific_tennis_event = tennis_events[5]" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 7, 87 | "metadata": { 88 | "collapsed": false, 89 | "deletable": true, 90 | "editable": true 91 | }, 92 | "outputs": [], 93 | "source": [ 94 | "event_id = specific_tennis_event['id']" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 8, 100 | "metadata": { 101 | "collapsed": false, 102 | "deletable": true, 103 | "editable": true 104 | }, 105 | "outputs": [], 106 | "source": [ 107 | "all_markets = mb.market_data.get_markets(event_id)" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 9, 113 | "metadata": { 114 | "collapsed": true, 115 | "deletable": true, 116 | "editable": true 117 | }, 118 | "outputs": [], 119 | "source": [ 120 | "ml_market_id = [m['id'] for m in all_markets if m['market-type']=='money_line'][0]" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 11, 126 | "metadata": { 127 | "collapsed": false, 128 | "deletable": true, 129 | "editable": true 130 | }, 131 | "outputs": [], 132 | "source": [ 133 | "ml_market_data = mb.market_data.get_runners(event_id, ml_market_id)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 13, 139 | "metadata": { 140 | "collapsed": true, 141 | "deletable": true, 142 | "editable": true 143 | }, 144 | "outputs": [], 145 | "source": [ 146 | "runner_id = ml_market_data['runners'][0]['id']" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": 15, 152 | "metadata": { 153 | "collapsed": true 154 | }, 155 | "outputs": [], 156 | "source": [ 157 | "order_insert = mb.betting.send_orders(runner_id, 2.0, Side.Back, 5.0)" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 19, 163 | "metadata": { 164 | "collapsed": true 165 | }, 166 | "outputs": [], 167 | "source": [ 168 | "order_amend = mb.betting.amend_orders(order_insert[0]['id'], 2.0, Side.Back, 2.0)" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 21, 174 | "metadata": { 175 | "collapsed": true 176 | }, 177 | "outputs": [], 178 | "source": [ 179 | "delete_order = mb.betting.delete_order(order_insert[0]['id'])" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 16, 185 | "metadata": { 186 | "collapsed": true, 187 | "deletable": true, 188 | "editable": true 189 | }, 190 | "outputs": [], 191 | "source": [ 192 | "mb.keep_alive()" 193 | ] 194 | } 195 | ], 196 | "metadata": { 197 | "anaconda-cloud": {}, 198 | "kernelspec": { 199 | "display_name": "Python 3", 200 | "language": "python", 201 | "name": "python3" 202 | }, 203 | "language_info": { 204 | "codemirror_mode": { 205 | "name": "ipython", 206 | "version": 3.0 207 | }, 208 | "file_extension": ".py", 209 | "mimetype": "text/x-python", 210 | "name": "python", 211 | "nbconvert_exporter": "python", 212 | "pygments_lexer": "ipython3", 213 | "version": "3.6.0" 214 | } 215 | }, 216 | "nbformat": 4, 217 | "nbformat_minor": 0 218 | } -------------------------------------------------------------------------------- /matchbook/tests/resources/marketdata_events.json: -------------------------------------------------------------------------------- 1 | {'allow-live-betting': True, 2 | 'category-id': [9, 3 | 297063445660036, 4 | 399952692940010, 5 | 410468520880009, 6 | 548851381700032, 7 | 553671759780032, 8 | 553823917040032], 9 | 'event-participants': [{'event-id': 553823925620032, 10 | 'id': 553823925680009, 11 | 'number': 1, 12 | 'participant-name': 'T Paul'}, 13 | {'event-id': 553823925620032, 14 | 'id': 553823925690032, 15 | 'number': 2, 16 | 'participant-name': 'C Ruud'}], 17 | 'id': 553823925620032, 18 | 'in-running-flag': True, 19 | 'markets': [{'allow-live-betting': True, 20 | 'back-overround': 100.3716, 21 | 'event-id': 553823925620032, 22 | 'id': 553823925780009, 23 | 'in-running-flag': True, 24 | 'lay-overround': 99.6284, 25 | 'live': True, 26 | 'market-type': 'money_line', 27 | 'name': 'Moneyline', 28 | 'runners': [{'event-id': 553823925620032, 29 | 'event-participant-id': 553823925680009, 30 | 'id': 553823925840032, 31 | 'market-id': 553823925780009, 32 | 'name': 'T Paul', 33 | 'prices': [{'available-amount': 38.81416, 34 | 'currency': 'EUR', 35 | 'decimal-odds': 2.31, 36 | 'exchange-type': 'back-lay', 37 | 'odds': 2.31, 38 | 'odds-type': 'DECIMAL', 39 | 'side': 'back'}, 40 | {'available-amount': 219.29122, 41 | 'currency': 'EUR', 42 | 'decimal-odds': 2.18, 43 | 'exchange-type': 'back-lay', 44 | 'odds': 2.18, 45 | 'odds-type': 'DECIMAL', 46 | 'side': 'back'}, 47 | {'available-amount': 31.05113, 48 | 'currency': 'EUR', 49 | 'decimal-odds': 2.07, 50 | 'exchange-type': 'back-lay', 51 | 'odds': 2.07, 52 | 'odds-type': 'DECIMAL', 53 | 'side': 'back'}, 54 | {'available-amount': 1337.54938, 55 | 'currency': 'EUR', 56 | 'decimal-odds': 2.33, 57 | 'exchange-type': 'back-lay', 58 | 'odds': 2.33, 59 | 'odds-type': 'DECIMAL', 60 | 'side': 'lay'}, 61 | {'available-amount': 2237.50188, 62 | 'currency': 'EUR', 63 | 'decimal-odds': 2.38, 64 | 'exchange-type': 'back-lay', 65 | 'odds': 2.38, 66 | 'odds-type': 'DECIMAL', 67 | 'side': 'lay'}, 68 | {'available-amount': 200.44133, 69 | 'currency': 'EUR', 70 | 'decimal-odds': 2.4, 71 | 'exchange-type': 'back-lay', 72 | 'odds': 2.4, 73 | 'odds-type': 'DECIMAL', 74 | 'side': 'lay'}], 75 | 'status': 'open', 76 | 'volume': 26525.63724}, 77 | {'event-id': 553823925620032, 78 | 'event-participant-id': 553823925690032, 79 | 'id': 553823925840009, 80 | 'market-id': 553823925780009, 81 | 'name': 'C Ruud', 82 | 'prices': [{'available-amount': 1778.94068, 83 | 'currency': 'EUR', 84 | 'decimal-odds': 1.75187, 85 | 'exchange-type': 'back-lay', 86 | 'odds': 1.75187, 87 | 'odds-type': 'DECIMAL', 88 | 'side': 'back'}, 89 | {'available-amount': 3087.7526, 90 | 'currency': 'EUR', 91 | 'decimal-odds': 1.72463, 92 | 'exchange-type': 'back-lay', 93 | 'odds': 1.72463, 94 | 'odds-type': 'DECIMAL', 95 | 'side': 'back'}, 96 | {'available-amount': 280.61786, 97 | 'currency': 'EUR', 98 | 'decimal-odds': 1.71428, 99 | 'exchange-type': 'back-lay', 100 | 'odds': 1.71428, 101 | 'odds-type': 'DECIMAL', 102 | 'side': 'back'}, 103 | {'available-amount': 50.84656, 104 | 'currency': 'EUR', 105 | 'decimal-odds': 1.76336, 106 | 'exchange-type': 'back-lay', 107 | 'odds': 1.76336, 108 | 'odds-type': 'DECIMAL', 109 | 'side': 'lay'}, 110 | {'available-amount': 258.76364, 111 | 'currency': 'EUR', 112 | 'decimal-odds': 1.84746, 113 | 'exchange-type': 'back-lay', 114 | 'odds': 1.84746, 115 | 'odds-type': 'DECIMAL', 116 | 'side': 'lay'}, 117 | {'available-amount': 33.22471, 118 | 'currency': 'EUR', 119 | 'decimal-odds': 1.93458, 120 | 'exchange-type': 'back-lay', 121 | 'odds': 1.93458, 122 | 'odds-type': 'DECIMAL', 123 | 'side': 'lay'}], 124 | 'status': 'open', 125 | 'volume': 23347.00139}], 126 | 'start': '2017-07-31T20:09:00.000Z', 127 | 'status': 'open', 128 | 'type': 'binary', 129 | 'volume': 49872.63863}], 130 | 'meta-tags': [{'id': 9, 131 | 'name': 'Tennis', 132 | 'type': 'SPORT', 133 | 'url-name': 'tennis'}, 134 | {'id': 297063445660036, 'name': 'R1', 'type': 'DATE', 'url-name': 'r1'}, 135 | {'id': 399952692940010, 136 | 'name': 'United States of America', 137 | 'type': 'COUNTRY', 138 | 'url-name': 'united-states-of-america'}, 139 | {'id': 410468520880009, 140 | 'name': 'Live Betting', 141 | 'type': 'OTHER', 142 | 'url-name': 'live-betting'}, 143 | {'id': 548851381700032, 144 | 'name': 'July 31st 2017', 145 | 'type': 'DATE', 146 | 'url-name': 'july-31st-2017'}, 147 | {'id': 553671759780032, 148 | 'name': 'ATP Washington', 149 | 'type': 'COMPETITION', 150 | 'url-name': 'atp-washington'}, 151 | {'id': 553823917040032, 152 | 'name': 'ATP Citi Open', 153 | 'type': 'COMPETITION', 154 | 'url-name': 'atp-citi-open'}], 155 | 'name': 'T Paul vs C Ruud', 156 | 'sport-id': 9, 157 | 'start': '2017-07-31T20:09:00.000Z', 158 | 'status': 'open', 159 | 'volume': 50897.02064} -------------------------------------------------------------------------------- /matchbook/endpoints/referencedata.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from matchbook.utils import clean_locals 4 | from matchbook.endpoints.baseendpoint import BaseEndpoint 5 | from matchbook import resources 6 | from matchbook.enums import SportsOrder, MarketStates 7 | 8 | 9 | class ReferenceData(BaseEndpoint): 10 | 11 | def get_currencies(self, session=None): 12 | """ 13 | Get the accepted currencies on betting platform. 14 | 15 | :param session: requests session to be used. 16 | :type session: requests.Session 17 | :returns: Supported currencies. 18 | :rtype: json 19 | :raises: MatchbookAPI.bin.exceptions.ApiError 20 | """ 21 | date_time_sent = datetime.datetime.utcnow() 22 | response = self.request("GET", self.client.urn_main, 'lookups/currencies', session=session) 23 | return self.process_response( 24 | response.json().get('currencies', []), resources.Currencies, date_time_sent, datetime.datetime.utcnow() 25 | ) 26 | 27 | def get_sports(self, status=MarketStates.All, order=SportsOrder.NameAsc, per_page=500, session=None): 28 | """ 29 | Lookup all sports, filter for active only/all. 30 | 31 | :param status: filter results by sport status, default 'active'. 32 | :type status: MatchbookAPI.bin.enums.SportStatus 33 | :param order: order in which results are returned, default 'name asc'. 34 | :type order: MatchbookAPI.bin.enums.SportsOrder 35 | :param per_page: no. of sports returned in a single response, Max 500. Default 20. 36 | :type per_page: int 37 | :param session: requests session to be used. 38 | :type session: requests.Session 39 | :return: Sports that are active on the betting platform. 40 | :rtype: json 41 | :raises: MatchbookAPI.bin.exceptions.ApiError 42 | """ 43 | params = clean_locals(locals()) 44 | date_time_sent = datetime.datetime.utcnow() 45 | response = self.request("GET", self.client.urn_edge, 'lookups/sports', params=params, session=session) 46 | return self.process_response( 47 | response.json().get('sports', []), resources.SportsDetails, date_time_sent, datetime.datetime.utcnow() 48 | ) 49 | 50 | def get_oddstype(self, session=None): 51 | """ 52 | Get the accepted odds-types. 53 | 54 | :param session: requests session to be used. 55 | :type session: requests.Session 56 | :returns: Supported odds-types. 57 | :rtype: json 58 | :raises: MatchbookAPI.bin.exceptions.ApiError 59 | """ 60 | date_time_sent = datetime.datetime.utcnow() 61 | response = self.request("GET", self.client.urn_main, 'lookups/odds-types', session=session) 62 | return self.process_response( 63 | response.json().get('odds-types', []), resources.OddsType, date_time_sent, datetime.datetime.utcnow() 64 | ) 65 | 66 | def get_countries(self, session=None): 67 | """ 68 | Get the countries and their relevant codes and ids. 69 | 70 | :param session: requests session to be used. 71 | :type session: requests.Session 72 | :returns: Countries and their details. 73 | :rtype: json 74 | :raises: MatchbookAPI.bin.exceptions.ApiError 75 | """ 76 | date_time_sent = datetime.datetime.utcnow() 77 | response = self.request("GET", self.client.urn_main, 'lookups/countries', session=session) 78 | return self.process_response( 79 | response.json().get('countries', []), resources.Countries, date_time_sent, datetime.datetime.utcnow() 80 | ) 81 | 82 | def get_regions(self, country_id, session=None): 83 | """ 84 | Get regions for a given country and their relevant codes and ids. 85 | 86 | :param country_id: id of the country whose regions we want to get. 87 | :type country_id: int 88 | :param session: requests session to be used. 89 | :type session: requests.Session 90 | :returns: Countries and their details. 91 | :rtype: json 92 | :raises: MatchbookAPI.bin.exceptions.ApiError 93 | """ 94 | date_time_sent = datetime.datetime.utcnow() 95 | response = self.request("GET", self.client.urn_main, 'lookups/regions/%s' % country_id, session=session) 96 | return self.process_response( 97 | response.json().get('countries', []), resources.Regions, date_time_sent, datetime.datetime.utcnow() 98 | ) 99 | 100 | def get_navigation(self, offset=0, per_page=500, session=None): 101 | # TODO: Map meta-tags to a resource. 102 | """ 103 | Get page navigation tree breakdown. 104 | 105 | :param offset: starting point of results. Default 0. 106 | :type offset: int 107 | :param per_page: no. of offers returned in a single response, Max 500. Default 20. 108 | :type per_page: int 109 | :param session: requests session to be used. 110 | :type session: requests.Session 111 | :returns: Orders data 112 | :raises: MatchbookAPI.bin.exceptions.ApiError 113 | """ 114 | params = clean_locals(locals()) 115 | date_time_sent = datetime.datetime.utcnow() 116 | response = self.request("GET", self.client.urn_edge, 'navigation', params=params, session=session) 117 | return self.process_response( 118 | response.json().get('countries', []), resources.MetaTags, date_time_sent, datetime.datetime.utcnow() 119 | ) 120 | -------------------------------------------------------------------------------- /matchbook/resources/reportresources.py: -------------------------------------------------------------------------------- 1 | from matchbook.resources.baseresource import BaseResource 2 | 3 | 4 | class BetReport(BaseResource): 5 | class Meta(BaseResource.Meta): 6 | attributes = { 7 | 'id': 'id', 8 | 'event-id': 'event-id', 9 | 'event-name': 'event-name', 10 | 'exchange-type': 'exchange-type', 11 | 'market-id': 'market-id', 12 | 'market-type': 'market-type', 13 | 'odds': 'odds', 14 | 'offer-id': 'offer-id', 15 | 'profit-and-loss': 'profit-and-loss', 16 | 'runner-id': 'runner-id', 17 | 'selection': 'selection', 18 | 'side': 'side', 19 | 'sport-id': 'sport-id', 20 | 'stake': 'stake', 21 | 'submitted-at': 'submitted-at', 22 | } 23 | datetime_attributes = ( 24 | 'submitted-at' 25 | ) 26 | 27 | 28 | class SettlementBet(BaseResource): 29 | class Meta(BaseResource.Meta): 30 | identifier = 'bets' 31 | attributes = { 32 | 'profit-and-loss': 'profit-and-loss', 33 | 'matched-time': 'matched-time', 34 | 'odds': 'odds', 35 | 'commission-rate': 'commission-rate', 36 | 'in-play': 'in-play', 37 | 'offer-id': 'offer-id', 38 | 'id': 'id', 39 | 'settled-time': 'settled-time', 40 | 'stake': 'stake', 41 | 'commission': 'commission' 42 | } 43 | 44 | 45 | class SettlementSelection(BaseResource): 46 | class Meta(BaseResource.Meta): 47 | identifier = 'selections' 48 | attributes = { 49 | 'name': 'name', 50 | 'odds': 'odds', 51 | 'profit-and-loss': 'profit-and-loss', 52 | 'id': 'id', 53 | 'side': 'side', 54 | 'stake': 'stake', 55 | 'commission': 'commission' 56 | } 57 | sub_resources = { 58 | 'bets': SettlementBet 59 | } 60 | 61 | 62 | class SettlementMarket(BaseResource): 63 | class Meta(BaseResource.Meta): 64 | identifier = 'markets' 65 | attributes = { 66 | 'profit-and-loss': 'profit-and-loss', 67 | 'net-win-commission': 'net-win-commission', 68 | 'name': 'name', 69 | 'id': 'id', 70 | 'commission': 'commission', 71 | 'stake': 'stake', 72 | } 73 | sub_resources = { 74 | 'selections': SettlementSelection 75 | } 76 | 77 | 78 | class SettlementEvent(BaseResource): 79 | class Meta(BaseResource.Meta): 80 | identifier = 'events' 81 | attributes = { 82 | 'sport-url': 'sport-url', 83 | 'start-time': 'start-time', 84 | 'sport-name': 'sport-name', 85 | 'name': 'name', 86 | 'id': 'id', 87 | 'sport-id': 'sport-id', 88 | 'finished-dead-heat': 'finished-dead-heat' 89 | } 90 | sub_resources = { 91 | 'markets': SettlementMarket 92 | } 93 | 94 | 95 | class SettlementReport(BaseResource): 96 | class Meta(BaseResource.Meta): 97 | attributes = { 98 | 'offset': 'offset', 99 | 'profit-and-loss': 'profit-and-loss', 100 | 'odds-type': 'odds-type', 101 | 'total': 'total', 102 | 'per-page': 'per-page', 103 | 'language': 'language', 104 | 'overall-staked-amount': 'overall-staked-amount' 105 | } 106 | sub_resources = { 107 | 'events': SettlementEvent 108 | } 109 | datetime_attributes = ( 110 | 'settled-at' 111 | ) 112 | 113 | 114 | class MarketSettlementReport(BaseResource): 115 | class Meta(BaseResource.Meta): 116 | attributes = { 117 | 'exchange-type': 'exchange-type', 118 | 'odds': 'odds', 119 | 'profit-and-loss': 'profit-and-loss', 120 | 'runner-id': 'runner-id', 121 | 'runner-name': 'runner-name', 122 | 'side': 'side', 123 | 'stake': 'stake', 124 | 'settled-at': 'settled-at', 125 | } 126 | datetime_attributes = ( 127 | 'settled-at' 128 | ) 129 | 130 | 131 | class RunnerSettlementReport(BaseResource): 132 | class Meta(BaseResource.Meta): 133 | attributes = { 134 | 'bet-id': 'bet-id', 135 | 'exchange-type': 'exchange-type', 136 | 'market-id': 'market-id', 137 | 'odds': 'odds', 138 | 'offer-id': 'offer-id', 139 | 'offer-type': 'offer-type', 140 | 'placed-at': 'placed-at', 141 | 'profit-and-loss': 'profit-and-loss', 142 | 'runner-id': 'runner-id', 143 | 'runner-name': 'runner-name', 144 | 'settled-at': 'settled-at', 145 | 'side': 'side', 146 | 'stake': 'stake', 147 | 'trade-type': 'trade-type', 148 | } 149 | datetime_attributes = ( 150 | 'settled-at', 151 | 'placed-at' 152 | ) 153 | 154 | 155 | class CommissionReport(BaseResource): 156 | class Meta(BaseResource.Meta): 157 | attributes = { 158 | 'commission': 'commission', 159 | 'commissionable-handle': 'commissionable-handle', 160 | 'offer-type': 'offer-type', 161 | 'placed-at': 'placed-at', 162 | 'rate': 'rate', 163 | 'runner-id': 'runner-id', 164 | 'runner-name': 'runner-name', 165 | 'settled-at': 'settled-at', 166 | 'trade-type': 'trade-type', 167 | } 168 | datetime_attributes = ( 169 | 'settled-at', 170 | 'placed-at' 171 | ) 172 | 173 | 174 | class TransactionReport(BaseResource): 175 | class Meta(BaseResource.Meta): 176 | attributes = { 177 | 'id': 'id', 178 | 'balance': 'balance', 179 | 'category': 'category', 180 | 'debit': 'debit', 181 | 'detail': 'detail', 182 | 'time-settled': 'time-settled', 183 | 'type': 'type', 184 | } 185 | datetime_attributes = ( 186 | 'time-settled' 187 | ) -------------------------------------------------------------------------------- /matchbook/tests/test_betting.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import unittest.mock as mock 4 | 5 | from matchbook.apiclient import APIClient 6 | from matchbook.endpoints.betting import Betting 7 | from matchbook.enums import Side, Status, AggregationType 8 | 9 | 10 | class BettingTest(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.client = APIClient('username', 'password') 14 | self.betting = Betting(self.client) 15 | 16 | @mock.patch('matchbook.endpoints.betting.Betting.process_response') 17 | @mock.patch('matchbook.endpoints.betting.Betting.request', return_value=mock.Mock()) 18 | def test_get_orders(self, mock_request, mock_process_response): 19 | self.betting.get_orders(event_ids=None, market_ids=None, runner_ids=None, offer_id=None, offset=0, per_page=500, 20 | interval=None, side=Side.Default, status=Status.Default, session=None) 21 | 22 | mock_request.assert_called_once_with( 23 | "GET", self.client.urn_edge, 'offers', session=None, target='offers', 24 | params={'offset': 0, 'per-page': 500, 'exchange-type': self.client.exchange_type}, 25 | ) 26 | assert mock_process_response.call_count == 1 27 | 28 | @mock.patch('matchbook.endpoints.betting.Betting.process_response') 29 | @mock.patch('matchbook.endpoints.betting.Betting.request', return_value=mock.Mock()) 30 | def test_get_orders_single(self, mock_request, mock_process_response): 31 | self.betting.get_orders(event_ids=None, market_ids=None, runner_ids=None, offer_id=123, offset=0, per_page=500, 32 | interval=None, side=Side.Default, status=Status.Default, session=None) 33 | 34 | mock_request.assert_called_once_with( 35 | "GET", self.client.urn_edge, 'offers/123', session=None, params={'odds-type': self.client.odds_type} 36 | ) 37 | assert mock_process_response.call_count == 1 38 | 39 | @mock.patch('matchbook.endpoints.betting.Betting.process_response') 40 | @mock.patch('matchbook.endpoints.betting.Betting.request', return_value=mock.Mock()) 41 | def test_send_orders(self, mock_request, mock_process_response): 42 | self.betting.send_orders(runner_id=1111, odds=2.0, side=Side.Back, stake=10.0, temp_id=None, session=None) 43 | 44 | mock_request.assert_called_once_with( 45 | "POST", self.client.urn_edge, 'offers', session=None, 46 | data={'offers': [{'runner-id': 1111, 'side': 'back', 'stake': 10.0, 'odds': 2.0, 'temp-id': None}], 47 | 'odds-type': self.client.odds_type, 48 | 'exchange-type': self.client.exchange_type, 49 | 'currency': self.client.currency} 50 | ) 51 | assert mock_process_response.call_count == 1 52 | 53 | 54 | @mock.patch('matchbook.endpoints.betting.Betting.process_response') 55 | @mock.patch('matchbook.endpoints.betting.Betting.request', return_value=mock.Mock()) 56 | def test_get_aggregate_bets(self, mock_request, mock_process_response): 57 | self.betting.get_agg_matched_bets(event_ids='1,2,3', market_ids=None, runner_ids=None, side=None, offset=0, 58 | per_page=500, aggregation_type=AggregationType.Default, session=None) 59 | 60 | mock_request.assert_called_once_with( 61 | "GET", self.client.urn_edge, 'bets/matched/aggregated', session=None, target='bets', 62 | params={'event-ids': '1,2,3', 'per-page': 500, 'offset': 0, 'aggregation-type': 'average'} 63 | ) 64 | assert mock_process_response.call_count == 1 65 | 66 | @mock.patch('matchbook.endpoints.betting.Betting.process_response') 67 | @mock.patch('matchbook.endpoints.betting.Betting.request', return_value=mock.Mock()) 68 | def test_get_positions(self, mock_request, mock_process_response): 69 | self.betting.get_positions( 70 | event_ids=None, market_ids='1,2', runner_ids=None, offset=0, per_page=500, session=None 71 | ) 72 | 73 | mock_request.assert_called_once_with( 74 | "GET", self.client.urn_edge, 'accounts/positions', session=None, 75 | params={'market-ids': '1,2', 'per-page': 500, 'offset': 0} 76 | ) 77 | assert mock_process_response.call_count == 1 78 | 79 | @mock.patch('matchbook.endpoints.betting.Betting.process_response') 80 | @mock.patch('matchbook.endpoints.betting.Betting.request', return_value=mock.Mock()) 81 | def test_amend_orders(self, mock_request, mock_process_response): 82 | self.betting.amend_orders( 83 | order_id=[1111, 1112], odds=[2.0, 1.9], side=[Side.Back, Side.Lay], stake=[10.0, 10.0], session=None 84 | ) 85 | 86 | mock_request.assert_called_once_with( 87 | "PUT", self.client.urn_edge, 'offers', session=None, 88 | data={'offers': [{'id': 1111, 'side': 'back', 'stake': 10.0, 'odds': 2.0}, 89 | {'id': 1112, 'side': 'lay', 'stake': 10.0, 'odds': 1.9}], 90 | 'odds-type': self.client.odds_type, 91 | 'exchange-type': self.client.exchange_type, 92 | 'currency': self.client.currency} 93 | ) 94 | assert mock_process_response.call_count == 1 95 | 96 | @mock.patch('matchbook.endpoints.betting.Betting.process_response') 97 | @mock.patch('matchbook.endpoints.betting.Betting.request', return_value=mock.Mock()) 98 | def test_delete_bulk_orders(self, mock_request, mock_process_response): 99 | self.betting.delete_bulk_orders(market_ids='142,152342') 100 | 101 | mock_request.assert_called_once_with( 102 | "DELETE", self.client.urn_edge, 'offers', session=None, data={'market-ids': '142,152342'}, 103 | ) 104 | assert mock_process_response.call_count == 1 105 | 106 | @mock.patch('matchbook.endpoints.betting.Betting.process_response') 107 | @mock.patch('matchbook.endpoints.betting.Betting.request', return_value=mock.Mock()) 108 | def test_delete_order(self, mock_request, mock_process_response): 109 | self.betting.delete_order(offer_id=1234) 110 | 111 | mock_request.assert_called_once_with("DELETE", self.client.urn_edge, 'offers/1234', session=None) 112 | assert mock_process_response.call_count == 1 113 | -------------------------------------------------------------------------------- /matchbook/tests/test_marketdata.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import unittest.mock as mock 4 | 5 | from matchbook.apiclient import APIClient 6 | from matchbook.endpoints.marketdata import MarketData 7 | from matchbook.enums import Side, MarketStates, Boolean 8 | 9 | 10 | class MarketDataTest(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.client = APIClient('username', 'password') 14 | self.market_data = MarketData(self.client) 15 | 16 | @mock.patch('matchbook.endpoints.marketdata.MarketData.process_response') 17 | @mock.patch('matchbook.endpoints.marketdata.MarketData.request', return_value=mock.Mock()) 18 | def test_get_events(self, mock_request, mock_process_response): 19 | self.market_data.get_events(sport_ids='9', states=MarketStates.All, per_page=500, offset=0, 20 | include_event_participants=Boolean.T, price_depth=3, side=Side.All) 21 | 22 | mock_request.assert_called_once_with( 23 | "GET", self.client.urn_edge, 'events', session=None, target='events', 24 | params={'sport-ids': '9', 'price-depth': 3, 'offset': 0, 'per-page': 500, 25 | 'exchange-type': self.client.exchange_type, 'include-event-participants': 'true', 26 | 'odds-type': self.client.odds_type, 'currency': self.client.currency}, 27 | ) 28 | assert mock_process_response.call_count == 1 29 | 30 | @mock.patch('matchbook.endpoints.marketdata.MarketData.process_response') 31 | @mock.patch('matchbook.endpoints.marketdata.MarketData.request', return_value=mock.Mock()) 32 | def test_get_events_single(self, mock_request, mock_process_response): 33 | self.market_data.get_events(event_id=123, include_event_participants=Boolean.T, price_depth=3, side=Side.All) 34 | 35 | mock_request.assert_called_once_with( 36 | "GET", self.client.urn_edge, 'events/123', session=None, 37 | params={'price-depth': 3, 'exchange-type': self.client.exchange_type, 'include-event-participants': 'true', 38 | 'odds-type': self.client.odds_type, 'currency': self.client.currency}, 39 | ) 40 | assert mock_process_response.call_count == 1 41 | 42 | @mock.patch('matchbook.endpoints.marketdata.MarketData.process_response') 43 | @mock.patch('matchbook.endpoints.marketdata.MarketData.request', return_value=mock.Mock()) 44 | def test_get_markets(self, mock_request, mock_process_response): 45 | self.market_data.get_markets(event_id=123, offset=0, per_page=500, price_depth=3) 46 | 47 | mock_request.assert_called_once_with( 48 | "GET", self.client.urn_edge, 'events/123/markets', session=None, target='markets', 49 | params={'event-id': 123, 'price-depth': 3, 'offset': 0, 'per-page': 500, 'currency': self.client.currency, 50 | 'exchange-type': self.client.exchange_type, 'odds-type': self.client.odds_type}, 51 | ) 52 | assert mock_process_response.call_count == 1 53 | 54 | @mock.patch('matchbook.endpoints.marketdata.MarketData.process_response') 55 | @mock.patch('matchbook.endpoints.marketdata.MarketData.request', return_value=mock.Mock()) 56 | def test_get_markets_single(self, mock_request, mock_process_response): 57 | self.market_data.get_markets(event_id=123, market_id=345, offset=0, per_page=500, price_depth=3) 58 | 59 | mock_request.assert_called_once_with( 60 | "GET", self.client.urn_edge, 'events/123/markets/345', session=None, 61 | params={'event-id': 123, 'market-id': 345, 'price-depth': 3, 'currency': self.client.currency, 62 | 'exchange-type': self.client.exchange_type, 'odds-type': self.client.odds_type}, 63 | ) 64 | assert mock_process_response.call_count == 1 65 | 66 | @mock.patch('matchbook.endpoints.marketdata.MarketData.process_response') 67 | @mock.patch('matchbook.endpoints.marketdata.MarketData.request', return_value=mock.Mock()) 68 | def test_get_runners(self, mock_request, mock_process_response): 69 | self.market_data.get_runners(event_id=123, market_id=345, include_withdrawn=Boolean.T, include_prices=Boolean.T, 70 | price_depth=3, side=Side.All) 71 | 72 | mock_request.assert_called_once_with( 73 | "GET", self.client.urn_edge, 'events/123/markets/345/runners', session=None, target='runners', 74 | params={'event-id': 123, 'market-id': 345, 'price-depth': 3, 'include-withdrawn': 'true', 75 | 'include-prices': 'true', 'currency': self.client.currency, 76 | 'exchange-type': self.client.exchange_type, 'odds-type': self.client.odds_type}, 77 | ) 78 | assert mock_process_response.call_count == 1 79 | 80 | @mock.patch('matchbook.endpoints.marketdata.MarketData.process_response') 81 | @mock.patch('matchbook.endpoints.marketdata.MarketData.request', return_value=mock.Mock()) 82 | def test_get_runners_single(self, mock_request, mock_process_response): 83 | self.market_data.get_runners(event_id=123, market_id=345, runner_id=567, include_withdrawn=Boolean.T, 84 | include_prices=Boolean.T, price_depth=3) 85 | 86 | mock_request.assert_called_once_with( 87 | "GET", self.client.urn_edge, 'events/123/markets/345/runners/567', session=None, 88 | params={'event_id': 123, 'market-id': 345, 'runner-id': 567, 'price-depth': 3, 89 | 'include-prices': 'true', 'currency': self.client.currency, 'include-withdrawn': 'true', 90 | 'exchange-type': self.client.exchange_type, 'odds-type': self.client.odds_type}, 91 | ) 92 | assert mock_process_response.call_count == 1 93 | 94 | @mock.patch('matchbook.endpoints.marketdata.MarketData.process_response') 95 | @mock.patch('matchbook.endpoints.marketdata.MarketData.request', return_value=mock.Mock()) 96 | def test_get_popular_markets(self, mock_request, mock_process_response): 97 | self.market_data.get_popular_markets(price_depth=3, old_format=Boolean.F) 98 | 99 | mock_request.assert_called_once_with( 100 | "GET", self.client.urn_edge, 'popular-markets', session=None, 101 | params={'price-depth': 3, 'old-format': 'false', 'currency': self.client.currency, 102 | 'exchange-type': self.client.exchange_type, 'odds-type': self.client.odds_type}, 103 | ) 104 | assert mock_process_response.call_count == 1 105 | -------------------------------------------------------------------------------- /matchbook/endpoints/reporting.py: -------------------------------------------------------------------------------- 1 | 2 | import datetime 3 | 4 | from matchbook import resources 5 | from matchbook.utils import clean_locals 6 | from matchbook.endpoints.baseendpoint import BaseEndpoint 7 | from matchbook.enums import TransactionCategories, TransactionTypes, PeriodFilter 8 | 9 | 10 | class Reporting(BaseEndpoint): 11 | 12 | def get_old_transactions_report(self, offset=0, per_page=500, after=None, before=None, period=PeriodFilter.Default, 13 | categories=TransactionCategories.Exchange, session=None): 14 | """ 15 | Get paginated historical transactions, filtered by arguments specified 16 | 17 | :param offset: starting point from which the paginated results begin. Default 0. 18 | :type offset: int 19 | :param per_page: number of bets to be returned per call, max 500. Default 20. 20 | :type per_page: int 21 | :param after: event start time lower cutoff. Default 0. 22 | :type after: UNIX timestamp 23 | :param before: event start time upper cutoff. Default current time. 24 | :type before: UNIX timestamp 25 | :param period: filter for the amount of time to include in settlement search. 26 | :type period: matchbook.enums.PeriodFilter 27 | :param categories: where the transaction was incurred, casino or exchange. 28 | :type categories: matchbook.enums.TransactionCategories 29 | :param session: requests session to be used. 30 | :type session: requests.Session 31 | :returns: Historical transaction info. 32 | :rtype: json 33 | :raises: matchbook.exceptions.ApiError 34 | """ 35 | params = clean_locals(locals()) 36 | date_time_sent = datetime.datetime.utcnow() 37 | response = self.request( 38 | 'GET', self.client.urn_main, 'reports/transactions', params=params, target='transactions', session=session 39 | ) 40 | return self.process_response( 41 | response, resources.TransactionReport, 42 | date_time_sent, datetime.datetime.now() 43 | ) 44 | 45 | def get_new_transactions_report(self, transaction_type=TransactionTypes.All, offset=0, per_page=500, 46 | after=None, before=None, session=None): 47 | # TODO: Map resource and get example data for data_structures 48 | """ 49 | Get paginated historical transactions, filtered by arguments specified 50 | 51 | :param offset: starting point from which the paginated results begin. Default 0. 52 | :type offset: int 53 | :param per_page: number of bets to be returned per call, max 500. Default 20. 54 | :type per_page: int 55 | :param after: event start time lower cutoff. Default 0. 56 | :type after: UNIX timestamp 57 | :param before: event start time upper cutoff. Default current time. 58 | :type before: UNIX timestamp 59 | :param transaction_type: filter by type of transaction. 60 | :type transaction_type: matchbook.enums.TransactionTypes 61 | :param session: requests session to be used. 62 | :type session: requests.Session 63 | :returns: Historical transaction info. 64 | :rtype: json 65 | :raises: matchbook.exceptions.ApiError 66 | """ 67 | params = clean_locals(locals()) 68 | date_time_sent = datetime.datetime.utcnow() 69 | response = self.request( 70 | 'GET', self.client.urn_edge, 'reports/v1/transactions', params=params, 71 | target='transactions', session=session 72 | ) 73 | return self.process_response(response, resources.TransactionReport, date_time_sent, datetime.datetime.now()) 74 | 75 | def get_current_offers(self, sport_ids=None, event_ids=None, market_ids=None, offset=0, per_page=500, session=None): 76 | """ 77 | Get a list of current offers i.e. offers on markets yet to be settled. 78 | 79 | :param sport_ids: operate only on orders on specified sports. 80 | :type sport_ids: comma separated string 81 | :param event_ids: operate only on orders on specified events. 82 | :type event_ids: comma separated string 83 | :param market_ids: operate only on orders on specified markets. 84 | :type market_ids: comma separated string 85 | :param offset: starting point of results. Default 0. 86 | :type offset: int 87 | :param per_page: no. of offers returned in a single response, Max 500. Default 20. 88 | :type per_page: int 89 | :param session: requests session to be used. 90 | :type session: requests.Session 91 | :returns: Orders data 92 | :raises: MatchbookAPI.bin.exceptions.ApiError 93 | 94 | """ 95 | params = clean_locals(locals()) 96 | date_time_sent = datetime.datetime.utcnow() 97 | response = self.request( 98 | 'GET', self.client.urn_edge, 'reports/v1/offers/current', params=params, target='offers', session=session 99 | ) 100 | return self.process_response(response, resources.Order, date_time_sent, datetime.datetime.now()) 101 | 102 | def get_current_bets(self, sport_ids=None, event_ids=None, market_ids=None, offset=0, per_page=500, session=None): 103 | """ 104 | Get a list of current bets i.e. offers that have been matched on markets yet to be settled. 105 | 106 | :param sport_ids: operate only on orders on specified sports. 107 | :type sport_ids: comma separated string 108 | :param event_ids: operate only on orders on specified events. 109 | :type event_ids: comma separated string 110 | :param market_ids: operate only on orders on specified markets. 111 | :type market_ids: comma separated string 112 | :param offset: starting point of results. Default 0. 113 | :type offset: int 114 | :param per_page: no. of offers returned in a single response, Max 500. Default 20. 115 | :type per_page: int 116 | :param session: requests session to be used. 117 | :type session: requests.Session 118 | :returns: Orders data 119 | :raises: MatchbookAPI.bin.exceptions.ApiError 120 | 121 | """ 122 | params = clean_locals(locals()) 123 | date_time_sent = datetime.datetime.utcnow() 124 | response = self.request( 125 | 'GET', self.client.urn_edge, 'reports/v1/bets/current', params=params, target='bets', session=session 126 | ) 127 | return self.process_response(response, resources.BetReport, date_time_sent, datetime.datetime.now()) 128 | 129 | def get_settled_bets(self, sport_ids=None, event_ids=None, market_ids=None, before=None, after=None, 130 | offset=0, per_page=500, session=None): 131 | """ 132 | Get a list of settled bets. 133 | 134 | :param sport_ids: operate only on bets on specified sports. 135 | :type sport_ids: comma separated string 136 | :param event_ids: operate only on bets on specified events. 137 | :type event_ids: comma separated string 138 | :param market_ids: operate only on bets on specified markets. 139 | :type market_ids: comma separated string 140 | :param after: event start time lower cutoff. Default None. 141 | :type after: UNIX timestamp 142 | :param before: event start time upper cutoff. Default None. 143 | :type before: UNIX timestamp 144 | :param offset: starting point of results. Default 0. 145 | :type offset: int 146 | :param per_page: no. of offers returned in a single response, Max 500. Default 20. 147 | :type per_page: int 148 | :param session: requests session to be used. 149 | :type session: requests.Session 150 | :returns: Orders data 151 | :raises: MatchbookAPI.bin.exceptions.ApiError 152 | """ 153 | params = clean_locals(locals()) 154 | date_time_sent = datetime.datetime.utcnow() 155 | response = self.request( 156 | 'GET', self.client.urn_edge, 'reports/v1/bets/settled', params=params, target='bets', session=session 157 | ) 158 | return self.process_response(response, resources.BetReport, date_time_sent, datetime.datetime.now()) 159 | 160 | 161 | -------------------------------------------------------------------------------- /matchbook/enums.py: -------------------------------------------------------------------------------- 1 | class Side: 2 | ''' 3 | Specify what order types to return in order requests, dependant on exchange type, Default will return all. 4 | 5 | :var back: orders backing the runner, suitable for exchange type back-lay. 6 | :var lay: orders backing the runner, suitable for exchange type back-lay. 7 | :var win: orders to win, suitable for exchange type binary. 8 | :var loss: orders to loss, suitable for exchange type binary. 9 | ''' 10 | Back = 'back' 11 | Lay = 'lay' 12 | Win = 'win' 13 | Loss = 'loss' 14 | All = None 15 | Default = All 16 | 17 | 18 | class ExchangeType: 19 | ''' 20 | Format of odds/liquidity returned on both sides. 21 | ''' 22 | Binary = 'binary' 23 | BackLay = 'back-lay' 24 | Default = BackLay 25 | 26 | 27 | class Status: 28 | ''' 29 | Specify the order status to return, Default will return all. 30 | 31 | :var matched: Offer has been partially or fully matched. Returned status will be "matched" for fully matched or "open" for partially matched. 32 | :var unmatched: Offer still has some stake available to be matched but may be "open" or "paused". 33 | :var cancelled: Offer has been cancelled and can no longer be matched. 34 | :var open: Initial state of offer before being partially matched, fully matched or edited. 35 | :var paused: Offer still has some remaining stake but cannot be matched until unpaused. 36 | :var expired: Offer has expired and can no longer be matched. 37 | 38 | ''' 39 | Matched = 'matched' 40 | Unmatched = 'unmatched' 41 | Cancelled = 'cancelled' 42 | Expired = 'expired' 43 | Open = 'open' 44 | Paused = 'paused' 45 | All = None 46 | Default = All 47 | 48 | 49 | class OddsType: 50 | ''' 51 | Odds format to be used/returned. 52 | 53 | :var US: american odds format. 54 | :var DECIMAL: decimal odds format. 55 | :var %: probability. 56 | :var HK: hong kong odds format. 57 | :var MALAY: malay odds format. 58 | :var INDO: indonesian odds format. 59 | 60 | ''' 61 | US = 'US' 62 | Decimal = 'DECIMAL' 63 | Prob = '%' 64 | HongKong = 'HK' 65 | Malay = 'MALAY' 66 | Indo = 'INDO' 67 | Default = Decimal 68 | 69 | 70 | class Currency: 71 | ''' 72 | Currency selection for use in placing bets. 73 | 74 | :var EUR: EUR 75 | :var USD: USD 76 | :var GBP: GBP 77 | ''' 78 | EUR = 'EUR' 79 | USD = 'USD' 80 | GBP = 'GBP' 81 | Default = EUR 82 | 83 | 84 | class MarketStates: 85 | ''' 86 | Filter by market state. 87 | 88 | :var open: market is currently open. 89 | :var suspended: market is currently suspended. 90 | ''' 91 | Open = 'open' 92 | Suspended = 'suspended' 93 | All = None 94 | Default = All 95 | 96 | 97 | class RunnerStates: 98 | ''' 99 | Filter by runner state 100 | 101 | :var open: market is currently open. 102 | :var suspended: market is currently suspended. 103 | ''' 104 | Open = 'open' 105 | Suspended = 'suspended' 106 | All = None 107 | Default = All 108 | 109 | 110 | class MarketType: 111 | ''' 112 | Filter by market type 113 | 114 | :var multirunner: market has multirunner type. 115 | :var binary: market has a binary outcome. 116 | ''' 117 | MultipleRunners = 'multirunner' 118 | Binary = 'binary' 119 | All = None 120 | Default = All 121 | 122 | 123 | class GradingType: 124 | ''' 125 | Filter by grading type i.e. how a market settles. 126 | 127 | :var asian-handicap: A dual handicap market, where each handicap is graded as per point spread below. 128 | :var high-score-wins: Markets where the team scoring the most points or goals are graded as the winner. 129 | :var low-score-wins: Markets where the team scoring the least points or goals are graded as the winner. 130 | :var point-spread: Markets where the score in the game, and the handicap value, are factored in for grading purposes. 131 | :var point-total: Markets where the total of all scores in the game decide the grading. 132 | :var single-winner-wins: Other markets where the grading is decided manually. 133 | ''' 134 | AsianHandicap = 'asian-handicap' 135 | HighScore = 'high-score-wins' 136 | LowScore = 'low-score-wins' 137 | PointSpread = 'point-spread' 138 | PointTotal = 'point-total' 139 | SingleWinner = 'single-winner-wins' 140 | All = None 141 | Default = All 142 | 143 | 144 | class MarketOrder: 145 | ''' 146 | Determine how results are ordered when returned. 147 | 148 | :var start asc: order by start time ascending. 149 | :var start desc: order by start time descending. 150 | ''' 151 | Ascending = 'start asc' 152 | Descending = 'start desc' 153 | Default = Ascending 154 | 155 | 156 | class Boolean: 157 | ''' 158 | booleans in api are lower case string type rather than python boolean. 159 | ''' 160 | T = 'true' 161 | F = 'false' 162 | 163 | 164 | class PriceOrder: 165 | ''' 166 | Determine how prices are ordered when returned. 167 | 168 | :var price asc: order by price ascending. 169 | :var price desc: oder by price descending. 170 | ''' 171 | Ascending = 'price asc' 172 | Descending = 'price desc' 173 | Default = Descending 174 | 175 | 176 | class MarketNames: 177 | ''' 178 | Market names which can be used to filter results. 179 | 180 | :TODO Check all market names for validity. 181 | ''' 182 | Match = 'match' 183 | PointSpread = 'point spread' 184 | OverUnder = 'over/under' 185 | GameLine = 'gameline' 186 | Match1H = 'match 1H' 187 | GameLine1H = 'gameline 1H' 188 | OverUnder1H = 'over/under 1H' 189 | Winner = 'Winner' 190 | ML = 'Money Line' 191 | ml = 'moneyline' 192 | gl = 'goal line' 193 | NumberRounds = 'Number of Rounds' 194 | TotalRounds = 'Total Rounds' 195 | Totals = 'totals' 196 | ThreeWay = 'Three Way' 197 | Total = 'Total' 198 | RaceWinner = 'Race Winner' 199 | All = None 200 | Default = All 201 | 202 | 203 | class SequenceOrder: 204 | ''' 205 | Determine how markets are ordered when returned. 206 | 207 | :var start asc: sort by start time ascending. 208 | :var start desc: sort by start time descending. 209 | :var seq asc: sort in sequential order ascending. 210 | :var seq desc: sort in sequential order descending. 211 | ''' 212 | StartAscending = 'start asc' 213 | StartDescending = 'start desc' 214 | SeqAscending = 'seq asc' 215 | SeqDescending = 'seq desc' 216 | Default = SeqAscending 217 | 218 | 219 | class SportsOrder: 220 | ''' 221 | Determine how sports are ordered when returned. 222 | 223 | :var name asc: order alphabetically ascending. 224 | :var name desc: order alphabetically descending. 225 | :var id asc: order by id number ascending. 226 | :var id desc: order by id number descending. 227 | ''' 228 | NameAsc = 'name asc' 229 | NameDesc = 'name desc' 230 | IDAsc = 'id asc' 231 | IDDEsc = 'id desc' 232 | Default = NameAsc 233 | 234 | 235 | class SportStatus: 236 | ''' 237 | Filter sport search by status of sport. 238 | 239 | :var active: there are markets active on this sport. 240 | :var pending: there are no active markets on this sport. 241 | ''' 242 | Active = 'active' 243 | Pending = 'pending' 244 | Default = Active 245 | 246 | 247 | class BetsOrder: 248 | ''' 249 | Determine the order in which bet report is sorted. 250 | 251 | :var event asc: event name alphabetically ascending. 252 | :var event desc: event name alphabetically descending. 253 | :var selection asc: selection name alphabetically ascending. 254 | :var selection desc: selection name alphabetically descending. 255 | :var market asc: market name alphabetically ascending. 256 | :var market desc: market name alphabetically descending. 257 | :var bet-id asc: bet id numerically ascending. 258 | :var bet-id desc: bet id numerically descending. 259 | ''' 260 | EventAsc = 'event asc' 261 | EventDesc = 'event desc' 262 | SelectionAsc = 'selection asc' 263 | SelectionDesc = 'selection desc' 264 | MarketAsc = 'market asc' 265 | MarketDesc = 'market desc' 266 | IDAsc = 'bet-id asc' 267 | IDDesc = 'bet-id desc' 268 | 269 | 270 | class BetStatusFilter: 271 | ''' 272 | Filter bet report to return matched bets only or now. 273 | 274 | :var true: return only matched bets. 275 | :var false: return all bets. 276 | ''' 277 | MatchedOnly = 'true' 278 | All = 'false' 279 | Default = MatchedOnly 280 | 281 | 282 | class BetGrouping: 283 | ''' 284 | Group betting report to return average odds and total stake on runners or all bets individually. 285 | 286 | :var true: group bets by runner and average odds, sum stakes 287 | :var false: return all bets individually 288 | ''' 289 | GroupBets = 'true' 290 | IndividualBets = 'false' 291 | Default = IndividualBets 292 | 293 | 294 | class SizeFilter: 295 | """ 296 | Filter to only include data where a specified dataset meets this criteria. 297 | 298 | :var greater-than: values are greater than a specific cutoff. 299 | :var less-than: values are less than a specific cutoff. 300 | :var equals: values are equal to a specific value. 301 | """ 302 | GreaterThan = 'greater-than' 303 | LessThan = 'less-than' 304 | Equal = 'equals' 305 | All = None 306 | Default = All 307 | 308 | 309 | class PeriodFilter: 310 | """ 311 | Period to filter reports to include. 312 | 313 | :var today: include only today. 314 | :var yesterday: include only yesterday 315 | :var 1-day: include one calendar day. 316 | :var 2-day: include two calendar day. 317 | :var week: include one calendar week. 318 | :var month: include one calendar month. 319 | :var 1-month: include one calendar month. 320 | :var 3-month: include three calendar months. 321 | """ 322 | Today = 'today' 323 | Yesterday = 'yesterday' 324 | Days1 = '1-day' 325 | Days2 = '2-day' 326 | Week = 'week' 327 | Month = 'month' 328 | Month1 = '1-month' 329 | Month3 = '3-month' 330 | All = None 331 | Default = All 332 | 333 | 334 | class TransactionCategories: 335 | """ 336 | Categories a transaction can fall under. 337 | 338 | :var casino: transaction was made on the casino site. 339 | :var exchange: transaction was made on the exchange site. 340 | :var collosus: NFI. 341 | """ 342 | Casino = 'casino' 343 | Exchange = 'exchange' 344 | Collosus = 'collosus' 345 | Default = Exchange 346 | 347 | 348 | class TransactionTypes: 349 | """ 350 | Types of transactions. 351 | 352 | :var 353 | """ 354 | Payout = 'payout' 355 | Commission = 'commission' 356 | Transfer = 'transfer' 357 | Cancel = 'cancel' 358 | Manual = 'manual' 359 | Bonus = 'bonus' 360 | All = None 361 | Default = All 362 | 363 | 364 | class AggregationType: 365 | """ 366 | Method of aggregation to be used in grouping bets. 367 | """ 368 | Average = 'average' 369 | Summary = 'summary' 370 | Default = Average 371 | -------------------------------------------------------------------------------- /matchbook/endpoints/marketdata.py: -------------------------------------------------------------------------------- 1 | 2 | import datetime 3 | 4 | from matchbook import resources 5 | from matchbook.endpoints.baseendpoint import BaseEndpoint 6 | from matchbook.enums import Boolean, Side, MarketNames, MarketType, MarketStates 7 | from matchbook.utils import clean_locals 8 | 9 | 10 | class MarketData(BaseEndpoint): 11 | 12 | def get_events(self, event_id=None, before=None, after=None, sport_ids=None, category_ids=None, 13 | states=MarketStates.All, tag_url_names=None, per_page=500, offset=0, 14 | include_event_participants=Boolean.T, price_depth=3, side=Side.All, 15 | minimum_liquidity=None, session=None): 16 | """ 17 | Get paginated events. Results can be filtered using various different parameters. 18 | 19 | :param event_id: specific event id. Default None. 20 | :type event_id: int 21 | :param after: event start time lower cutoff. Default None. 22 | :type after: UNIX timestamp 23 | :param before: event start time upper cutoff. Default None. 24 | :type before: UNIX timestamp 25 | :param category_ids: filter results by category id. Default None. 26 | :type category_ids: comma separated string 27 | :param sport_ids: filter results by sports id(s). Default None. 28 | :type sport_ids: comma separated string 29 | :param states: filter results by event state or comma separated string of types. Default None. 30 | :type states: matchbook.enums.MarketStates 31 | :param tag_url_names:Only events with tags having url-name in the provided list are included in the response. 32 | :type tag_url_names: comma separated string 33 | :param per_page: number of results to show in a single result. Max=500. Default 20. 34 | :type per_page: int 35 | :param offset: starting page of results to show. Default 0. 36 | :type offset: int 37 | :param include_event_participants: A boolean indicating whether to return the event participants information 38 | :type include_event_participants: matchbook.enums.Boolean 39 | :param price_depth: max depth to be returned for prices. Default 3. 40 | :type price_depth: int 41 | :param side: filter results by side (dependent on exchange-type). Default None. 42 | :type side: matchbook.enums.Side 43 | :param minimum_liquidity: Only prices with available-amount greater than or equal to this value are included. 44 | :type minimum_liquidity: float 45 | :param session: requests session to be used. 46 | :type session: requests.Session 47 | :returns: Breakdown to each runner if they are included. 48 | :rtype: json 49 | :raises: matchbook.exceptions.ApiError 50 | """ 51 | params = clean_locals(locals()) 52 | date_time_sent = datetime.datetime.utcnow() 53 | method = 'events' 54 | params['odds-type'] = self.client.odds_type 55 | params['exchange-type'] = self.client.exchange_type 56 | params['currency'] = self.client.currency 57 | if event_id: 58 | method = 'events/%s' % event_id 59 | del_keys = ['event-id', 'after', 'before', 'category-ids', 'sport-ids', 60 | 'states', 'per-page', 'offset', 'tag-url-names'] 61 | params = {k: v for k, v in params.items() if k not in del_keys} 62 | response = self.request("GET", self.client.urn_edge, method, params=params, session=session) 63 | response = response.json().get('event', response.json()) 64 | else: 65 | response = self.request( 66 | "GET", self.client.urn_edge, method, params=params, target='events', session=session 67 | ) 68 | return self.process_response(response, resources.Event, date_time_sent, datetime.datetime.utcnow()) 69 | 70 | def get_markets(self, event_id, market_id=None, names=MarketNames.All, types=MarketType.All, offset=0, per_page=500, 71 | states=MarketStates.All, price_depth=3, side=Side.Default, minimum_liquidity=None, session=None): 72 | """ 73 | Get paginated markets for an event specified by the event_id. 74 | 75 | :param event_id: specific event id. 76 | :type event_id: int 77 | :param market_id: specific market id to pull data for. 78 | :type market_id: int 79 | :param states: filter results by market state or a comma separated string of states. Default 'open', 'suspended' 80 | :type states: matchbook.enums.MarketStates 81 | :param types: filter results by market type or a comma separated string of types. Default None. 82 | :type types: matchbook.enums.MarketType 83 | :param names: filter results by market name. Default None. 84 | :type names: matchbook.enums.MarketNames 85 | :param per_page: number of results to show in a single result. Max=500. Default 20. 86 | :type per_page: int 87 | :param offset: starting page of results to show. Default 0. 88 | :type offset: int 89 | :param price_depth: max depth to be returned for prices. Default 3. 90 | :type price_depth: int 91 | :param side: filter results by side (dependent on exchange-type). Default None. 92 | :type side: matchbook.enums.Side 93 | :param minimum_liquidity: Only prices with available-amount greater than or equal to this value are included. 94 | :type minimum_liquidity: float 95 | :param session: requests session to be used. 96 | :type session: requests.Session 97 | :returns: Breakdown of each runner if they are included. 98 | :rtype: json 99 | :raises: matchbook.exceptions.ApiError 100 | """ 101 | params = clean_locals(locals()) 102 | date_time_sent = datetime.datetime.utcnow() 103 | params['odds-type'] = self.client.odds_type 104 | params['exchange-type'] = self.client.exchange_type 105 | params['currency'] = self.client.currency 106 | method = 'events/%s/markets' % event_id 107 | if market_id: 108 | method = 'events/%s/markets/%s' % (event_id, market_id) 109 | del_keys = ['names', 'types', 'per-page', 'offset', 'states'] 110 | params = {k: v for k, v in params.items() if k not in del_keys} 111 | response = self.request('GET', self.client.urn_edge, method, params=params, session=session) 112 | response = response.json().get('market', response.json()) 113 | else: 114 | response = self.request( 115 | "GET", self.client.urn_edge, method, params=params, target='markets', session=session 116 | ) 117 | return self.process_response(response, resources.Market, date_time_sent, datetime.datetime.utcnow()) 118 | 119 | def get_runners(self, event_id, market_id, runner_id=None, states=MarketStates.All, include_withdrawn=Boolean.T, 120 | include_prices=Boolean.T, price_depth=3, side=Side.All, minimum_liquidity=None, session=None): 121 | """ 122 | Get runner data for an event and market specified by their ids. 123 | 124 | :param event_id: specific event id. 125 | :type event_id: int 126 | :param market_id: specific market id to pull data for. 127 | :type market_id: int 128 | :param runner_id: specific runner to pull data for. 129 | :type runner_id: int 130 | :param states: filter results by runner state or a comma separated string of states. Default 'open', 'suspended' 131 | :param include_withdrawn: boolean for returning or not the withdrawn runners in the response. 132 | :type include_withdrawn: matchbook.enums.Boolean 133 | :param include_prices: boolean indicating whether to return the prices for the runners. 134 | :type include_prices: matchbook.enums.Boolean 135 | :type states: matchbook.enums.MarketStates 136 | :param price_depth: max depth to be returned for prices. Default 3. 137 | :type price_depth: int 138 | :param side: filter results by side (dependent on exchange-type). Default None. 139 | :type side: matchbook.enums.Side 140 | :param minimum_liquidity: Only prices with available-amount greater than or equal to this value are included. 141 | :type minimum_liquidity: float 142 | :param session: requests session to be used. 143 | :type session: requests.Session 144 | :returns: Breakdown of each runner if they are included. 145 | :rtype: json 146 | :raises: matchbook.exceptions.ApiError 147 | """ 148 | params = clean_locals(locals()) 149 | date_time_sent = datetime.datetime.utcnow() 150 | params['odds-type'] = self.client.odds_type 151 | params['exchange-type'] = self.client.exchange_type 152 | params['currency'] = self.client.currency 153 | method = 'events/%s/markets/%s/runners' % (event_id, market_id) 154 | if runner_id: 155 | method = 'events/%s/markets/%s/runners/%s' % (event_id, market_id, runner_id) 156 | del_keys = ['include-withdraw', 'states'] 157 | params = {k: v for k, v in params.items() if k not in del_keys} 158 | response = self.request('GET', self.client.urn_edge, method, params=params, session=session) 159 | response = response.json().get('runner', response.json()) 160 | else: 161 | response = self.request( 162 | 'GET', self.client.urn_edge, method, params=params, target='runners', session=session 163 | ).json() 164 | return self.process_response(response, resources.Runner, date_time_sent, datetime.datetime.utcnow()) 165 | 166 | def get_popular_markets(self, price_depth=3, side=Side.All, minimum_liquidity=None, 167 | old_format=Boolean.F, session=None): 168 | """ 169 | Get popular markets as defined by matchbook. 170 | 171 | :param price_depth: max depth to be returned for prices. Default 3. 172 | :type price_depth: int 173 | :param side: filter results by side (dependent on exchange-type). Default None. 174 | :type side: matchbook.enums.Side 175 | :param minimum_liquidity: Only prices with available-amount greater than or equal to this value are included. 176 | :type minimum_liquidity: float 177 | :param old_format: 178 | :type old_format: 179 | :param session: requests session to be used. 180 | :type session: requests.Session 181 | :returns: Breakdown of each runner if they are included. 182 | :rtype: json 183 | :raises: matchbook.exceptions.ApiError 184 | """ 185 | params = clean_locals(locals()) 186 | date_time_sent = datetime.datetime.utcnow() 187 | params['odds-type'] = self.client.odds_type 188 | params['exchange-type'] = self.client.exchange_type 189 | params['currency'] = self.client.currency 190 | response = self.request('GET', self.client.urn_edge, 'popular-markets', params=params, session=session) 191 | return self.process_response( 192 | response.json().get('markets', response.json()), resources.Market, 193 | date_time_sent, datetime.datetime.utcnow() 194 | ) -------------------------------------------------------------------------------- /matchbook/metadata.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from pandas.io.json import json_normalize 3 | from matchbook.utils import clean_time 4 | import numpy as np 5 | import re 6 | 7 | 8 | def clean_bet_report(df): 9 | """ 10 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_bets_report 11 | 12 | :param df: MatchbookAPI.MatchbookAPI.get_bets_report raw data. 13 | :type df: Pandas.Dataframe 14 | :returns: cleaned bet report. 15 | :rtype: Pandas.Dataframe 16 | 17 | """ 18 | if 'bets' in df.columns: 19 | df = df.pipe(parser_function, sculpt_col, 'bets') 20 | df.rename(columns={'id':'offer-id'}, inplace=True) 21 | for _ in ['submitted-at', 'placed-at', 'settled-at']: 22 | if _ in df.columns: 23 | df[_] = clean_time(df[_]) 24 | return df 25 | 26 | 27 | def clean_commission_report(df): 28 | """ 29 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_commission_report 30 | 31 | :param df: MatchbookAPI.MatchbookAPI.get_commission_report raw data. 32 | :type df: Pandas.Dataframe 33 | :returns: cleaned commission report. 34 | :rtype: Pandas.Dataframe 35 | 36 | """ 37 | if 'sub-totals' in df.columns: 38 | df = df.pipe(parser_function, sculpt_col, 'sub-totals') 39 | if 'settled-at' in df.columns: 40 | df['settled-at'] = clean_time(df['settled-at']) 41 | if 'placed-at' in df.columns: 42 | df['placed-at'] = clean_time(df['placed-at']) 43 | return df 44 | 45 | 46 | def clean_transaction_report(df): 47 | """ 48 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_transactions_report 49 | 50 | :param df: MatchbookAPI.MatchbookAPI.get_transactions_report raw data. 51 | :type df: Pandas.Dataframe 52 | :returns: cleaned transactions report. 53 | :rtype: Pandas.Dataframe 54 | 55 | """ 56 | if 'transactions' in df.columns: 57 | df = df.pipe(parser_function, sculpt_col, 'transactions') 58 | return df 59 | 60 | 61 | def clean_orders(df): 62 | """ 63 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_orders 64 | 65 | :param df: MatchbookAPI.MatchbookAPI.get_orders raw data. 66 | :type df: Pandas.Dataframe 67 | :returns: cleaned orders report. 68 | :rtype: Pandas.Dataframe 69 | 70 | """ 71 | if 'offers' in df.columns: 72 | df = df.pipe(parser_function, sculpt_col, 'offers') 73 | df.rename(columns={'id': 'offer-id'}, inplace=True) 74 | if 'matched-bets' in df.columns: 75 | df = df.pipe(parser_function, sculpt_col, 'matched-bets') 76 | df.rename(columns={'id': 'bet-id'}, inplace=True) 77 | if 'created-at' in df.columns: 78 | df['created-at'] = clean_time(df['created-at']) 79 | return df 80 | 81 | 82 | def clean_settlement_report(df): 83 | """ 84 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_settlement_report 85 | 86 | :param df: MatchbookAPI.MatchbookAPI.get_settlement_report raw data. 87 | :type df: Pandas.Dataframe 88 | :returns: cleaned settlement report. 89 | :rtype: Pandas.Dataframe 90 | 91 | """ 92 | if 'markets' in df.columns: 93 | df = df.pipe(parser_function, sculpt_col, 'markets') 94 | if 'runners' in df.columns: 95 | df = df.pipe(parser_function, sculpt_col, 'runners') 96 | if 'settled-at' in df.columns: 97 | df['settled-at'] = clean_time(df['settled-at']) 98 | return df 99 | 100 | 101 | def parse_meta_tags(df): 102 | """ 103 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_navigation 104 | 105 | :param df: MatchbookAPI.MatchbookAPI.get_navigation raw data. 106 | :type df: Pandas.Dataframe 107 | :returns: cleaned navigation data. 108 | :rtype: Pandas.Dataframe 109 | 110 | """ 111 | if 'meta-tags' in df.columns: 112 | df = df.pipe(parser_function, sculpt_col, 'meta-tags') 113 | df.rename(columns={'id': 'tree-id1', 'name': 'tree-name1', 'url-name': 'tree-url1'}, inplace=True) 114 | if 'meta-tags' in df.columns: 115 | df = df.pipe(parser_function, sculpt_col, 'meta-tags') 116 | df.rename(columns={'id': 'type1-id', 'type': 'type1', 'name': 'type1-name', 'url-name': 'type1-url-name'}, 117 | inplace=True) 118 | if 'tree-id' in df.columns: 119 | df.drop('tree-id', axis=1, inplace=True) 120 | if 'meta-tags' in df.columns: 121 | df = df.pipe(parser_function, sculpt_col, 'meta-tags') 122 | df.rename(columns={'id': 'type2-id', 'type': 'type2', 'name': 'type2-name', 'url-name': 'type2-url-name'}, 123 | inplace=True) 124 | if 'meta-tags' in df.columns: 125 | df = df.pipe(parser_function, sculpt_col, 'meta-tags') 126 | df.rename(columns={'id': 'type3-id', 'type': 'type3', 'name': 'type3-name', 'url-name': 'type3-url-name'}, 127 | inplace=True) 128 | if 'meta-tags' in df.columns: 129 | df = df.pipe(parser_function, sculpt_col, 'meta-tags') 130 | return df 131 | 132 | 133 | def split_pricedata(df): 134 | """ 135 | Splits prices dictionary into individual columns by level. 136 | 137 | :param df: dataframe containing the pricing information to split. 138 | :type df: Dataframe 139 | :returns: pricing information. 140 | :rtype: Pandas.Series 141 | """ 142 | p_d = {} 143 | back_count = 0 144 | lay_count = 0 145 | for price in df.prices: 146 | if price['side'] == 'back': 147 | back_count += 1 148 | p_d['Back' + str(back_count)] = price['odds'] 149 | p_d['BackSize' + str(back_count)] = price['available-amount'] 150 | #p_d['Back' + str(price['odds'])] = price['available-amount'] 151 | elif price['side'] == 'lay': 152 | lay_count += 1 153 | p_d['Lay' + str(lay_count)] = price['odds'] 154 | p_d['LaySize' + str(lay_count)] = price['available-amount'] 155 | #p_d['Lay' + str(price['odds'])] = price['available-amount'] 156 | else: 157 | continue 158 | return pd.Series(p_d) 159 | 160 | 161 | def clean_event_col_types(df): 162 | """ 163 | Clean specific columns returned from events requests. 164 | 165 | :param df: data returned from events requests. 166 | :type df: Pandas.Dataframe 167 | :returns: data with clean start, handicap and asian-handicap columns. 168 | :rtype: Pandas.Dataframe 169 | 170 | """ 171 | all_cols = df.columns 172 | if 'start' in all_cols: 173 | df['start'] = clean_time(df['start']) 174 | if 'handicap' in all_cols: 175 | df['handicap'] = df.handicap.apply(lambda x: float(x) if x != '' else np.nan) 176 | if 'asian-handicap' in all_cols: 177 | df['asian-handicap'] = df['asian-handicap'].apply(lambda x: 178 | np.mean([float(y) for y in re.findall('\d\.\d', x)]) * 179 | (-1 if '-' in x else 1)) 180 | return df 181 | 182 | 183 | def parse_event_data(df): 184 | """ 185 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_events or MatchbookAPI.MatchbookAPI.get_event_allmarkets. 186 | 187 | :param df: MatchbookAPI.MatchbookAPI.get_events or MatchbookAPI.MatchbookAPI.get_event_allmarkets raw data. 188 | :type df: Pandas.Dataframe 189 | :returns: cleaned events data breakdown. 190 | :rtype: Pandas.Dataframe 191 | 192 | """ 193 | if 'events' in df.columns: 194 | df = df.pipe(parser_function, sculpt_col, 'events') 195 | df.rename(columns={'id': 'event-id', 'name': 'event-name'}, inplace=True) 196 | if 'category-id' in df.columns: 197 | df['category-id'] = df['category-id'].apply(lambda x: x[0]) 198 | if 'meta-tags' in df.columns: 199 | df.drop('meta-tags', axis=1, inplace=True) 200 | if 'markets' in df.columns: 201 | df = df.pipe(parser_function, sculpt_col, 'markets') 202 | df.rename(columns={'id': 'market-id', 'name': 'market-name'}, inplace=True) 203 | if 'runners' in df.columns: 204 | df = df.pipe(parser_function, sculpt_col, 'runners') 205 | df.rename(columns={'id': 'runner-id', 'name': 'runner-name'}, inplace=True) 206 | if 'prices' in df.columns: 207 | df = pd.concat([df, df.apply(split_pricedata, axis=1)], axis=1) 208 | df.drop('prices', axis=1, inplace=True) 209 | return df 210 | 211 | 212 | def parse_single_market(df): 213 | """ 214 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_event_singlemarket or MatchbookAPI.MatchbookAPI.get_event_marketrunners. 215 | 216 | :param df: MatchbookAPI.MatchbookAPI.get_event_singlemarket or MatchbookAPI.MatchbookAPI.get_event_marketrunners raw data. 217 | :type df: Pandas.Dataframe 218 | :returns: cleaned events data breakdown. 219 | :rtype: Pandas.Dataframe 220 | 221 | """ 222 | df.rename(columns={'id': 'market-id', 'name': 'market-name'}, inplace=True) 223 | if 'runners' in df.columns: 224 | df = df.pipe(parser_function, sculpt_col, 'runners') 225 | df.rename(columns={'id': 'runner-id', 'name': 'runner-name'}, inplace=True) 226 | if 'prices' in df.columns: 227 | df = pd.concat([df, df.apply(split_pricedata, axis=1)], axis=1) 228 | df.drop('prices', axis=1, inplace=True) 229 | return df 230 | 231 | 232 | def parse_single_runner(df): 233 | """ 234 | Clean raw data returned from MatchbookAPI.MatchbookAPI.get_event_singlerunner. 235 | 236 | :param df: MatchbookAPI.MatchbookAPI.get_event_singlerunner raw data. 237 | :type df: Pandas.Dataframe 238 | :returns: cleaned events data breakdown. 239 | :rtype: Pandas.Dataframe 240 | 241 | """ 242 | df.rename(columns={'id': 'runner-id', 'name': 'runner-name'}, inplace=True) 243 | if 'prices' in df.columns: 244 | df = pd.concat([df, df.apply(split_pricedata, axis=1)], axis=1) 245 | df.drop('prices', axis=1, inplace=True) 246 | return df 247 | 248 | 249 | def parser_function(df, parsing_function, col_name): 250 | """ 251 | Parse a specified dataframe column when provided with parsing breakdowns to use. 252 | 253 | :param df: dataframe which contains the column to be parsed 254 | :type df: Dataframe 255 | :param parsing_function: function to parse the specified col_name using. 256 | :type parsing_function: func 257 | :param col_name: column name in df that is to be parsed. 258 | :returns: flattened dataframe 259 | :rtype: Dataframe 260 | 261 | """ 262 | df[col_name] = df[col_name].map(lambda x: pd.DataFrame() if len(x) == 0 else json_normalize(x)) 263 | df = df.apply(parsing_function, args=(col_name,), axis=1) 264 | df = pd.concat([x for x in df[col_name].tolist()], ignore_index=True) 265 | df = df.fillna('') 266 | return df 267 | 268 | 269 | def sculpt_col(df, col_name): 270 | """ 271 | Flattens Dataframe where column is of type dataframe, applied cell by cell. 272 | 273 | :param df: 274 | :type df: dataframe row 275 | :param col_name: name of the column which is to be flattened. 276 | :type col_name: str 277 | :returns: flattened dataframe 278 | :rtype: Dataframe 279 | 280 | """ 281 | try: 282 | if len(df[col_name]) > 0: 283 | for key in df.keys().tolist(): 284 | if key != col_name: 285 | df[col_name][key] = df[key] 286 | else: 287 | keys = df.keys().tolist() 288 | keys.pop(keys.index(col_name)) 289 | df[col_name] = pd.DataFrame(columns=[key for key in keys], data=[[df[key] for key in keys]]) 290 | except: 291 | raise ValueError(df[col_name]) 292 | return df 293 | -------------------------------------------------------------------------------- /matchbook/endpoints/betting.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from matchbook.endpoints.baseendpoint import BaseEndpoint 4 | from matchbook import resources 5 | from matchbook.enums import Side, Status, AggregationType 6 | from matchbook.utils import clean_locals 7 | 8 | 9 | class Betting(BaseEndpoint): 10 | 11 | def get_orders(self, event_ids=None, market_ids=None, runner_ids=None, offer_id=None, offset=0, per_page=500, 12 | interval=None, side=Side.Default, status=Status.Default, session=None): 13 | """ 14 | Get all orders which fit the argument filters. 15 | 16 | :param event_ids: operate only on orders on specified events. 17 | :type event_ids: comma separated string 18 | :param market_ids: operate only on orders on specified markets. 19 | :type market_ids: comma separated string 20 | :param runner_ids: operate only on orders on specified runners. 21 | :type runner_ids: comma separated string 22 | :param offer_id: specific order id to use. 23 | :param offset: starting point of results. Default 0. 24 | :type offset: int 25 | :param per_page: no. of offers returned in a single response, Max 500. Default 20. 26 | :type per_page: int 27 | :param interval: check for orders updated/created in last x seconds, status param must be 'matched'. 28 | :type interval: int 29 | :param side: filter results by side (dependent on exchange-type). Default None. 30 | :type side: MatchbookAPI.bin.enums.Side 31 | :param status: operate only on orders with specified status. Default None. 32 | :type status: MatchbookAPI.bin.enums.Status 33 | :param session: requests session to be used. 34 | :type session: requests.Session 35 | :returns: Orders data 36 | :raises: MatchbookAPI.bin.exceptions.ApiError 37 | 38 | """ 39 | params = clean_locals(locals()) 40 | params['exchange-type'] = self.client.exchange_type 41 | method = 'offers' 42 | date_time_sent = datetime.datetime.utcnow() 43 | if offer_id: 44 | method = 'offers/{0}'.format(offer_id) 45 | params = {'odds-type': self.client.odds_type} 46 | response = self.request("GET", self.client.urn_edge, method, params=params, session=session).json() 47 | else: 48 | response = self.request( 49 | "GET", self.client.urn_edge, method, params=params, target='offers', session=session 50 | ) 51 | date_time_received = datetime.datetime.utcnow() 52 | return self.process_response(response, resources.Order, date_time_sent, date_time_received) 53 | 54 | def send_orders(self, runner_id, odds, side, stake, temp_id=None, session=None): 55 | """ 56 | Place an order(s) on a runner, multiple orders can be places by providing lists of the required arguments. 57 | 58 | :param runner_id: runner(s) on which to place bets. 59 | :type runner_id: int 60 | :param odds: odds at which we wish to place the bet. 61 | :type odds: float 62 | :param side: The type of bet to place, dependent on exchange. 63 | :type side: MatchbookAPI.bin.enums.Side 64 | :param stake: amount in account currency to place on the bet. 65 | :type stake: float 66 | :param temp_id: A helper ID generated by the client to help understand the correlation between multiple submitted offers and their responses. 67 | :type temp_id: str 68 | :param session: requests session to be used. 69 | :type session: requests.Session 70 | :returns: Orders responses, i.e. filled or at exchange or errors. 71 | :raises: MatchbookAPI.bin.exceptions.ApiError 72 | 73 | """ 74 | date_time_sent = datetime.datetime.utcnow() 75 | params = { 76 | 'offers': [], 77 | 'odds-type': self.client.odds_type, 78 | 'exchange-type': self.client.exchange_type, 79 | 'currency': self.client.currency, 80 | } 81 | if isinstance(runner_id, list): 82 | if isinstance(temp_id, list): 83 | for i, _ in enumerate(runner_id): 84 | params['offers'].append({'runner-id': runner_id[i], 'side': side[i], 'stake': stake[i], 85 | 'odds': odds[i], 'temp-id': temp_id[i]}) 86 | else: 87 | for i, _ in enumerate(runner_id): 88 | params['offers'].append({'runner-id': runner_id[i], 'side': side[i], 'stake': stake[i], 89 | 'odds': odds[i]}) 90 | else: 91 | params['offers'].append( 92 | {'runner-id': runner_id, 'side': side, 'stake': stake, 'odds': odds, 'temp-id': temp_id} 93 | ) 94 | method = 'offers' 95 | response = self.request("POST", self.client.urn_edge, method, data=params, session=session) 96 | date_time_received = datetime.datetime.utcnow() 97 | return self.process_response( 98 | response.json().get('offers', []), resources.Order, date_time_sent, date_time_received 99 | ) 100 | 101 | def get_agg_matched_bets(self, event_ids=None, market_ids=None, runner_ids=None, side=None, offset=0, per_page=500, 102 | aggregation_type=AggregationType.Default, session=None): 103 | # TODO: Make aggregate matched bets resource 104 | """ 105 | Get matched bets aggregated. 106 | 107 | :param event_ids: operate only on orders on specified events. 108 | :type event_ids: comma separated string 109 | :param market_ids: operate only on orders on specified markets. 110 | :type market_ids: comma separated string 111 | :param runner_ids: operate only on orders on specified runners. 112 | :type runner_ids: comma separated string 113 | :param offset: starting point of results. Default 0. 114 | :type offset: int 115 | :param per_page: no. of offers returned in a single response, Max 500. Default 20. 116 | :type per_page: int 117 | :param side: filter results by side (dependent on exchange-type). Default None. 118 | :type side: MatchbookAPI.bin.enums.Side 119 | :param aggregation_type: how to aggregate bets 120 | :type aggregation_type: matchbook.enums.AggregationType 121 | :param session: requests session to be used. 122 | :type session: requests.Session 123 | :returns: Orders data 124 | :raises: MatchbookAPI.bin.exceptions.ApiError 125 | 126 | """ 127 | params = clean_locals(locals()) 128 | date_time_sent = datetime.datetime.utcnow() 129 | method = 'bets/matched/aggregated' 130 | response = self.request("GET", self.client.urn_edge, method, params=params, target='bets', session=session) 131 | date_time_received = datetime.datetime.utcnow() 132 | return self.process_response( 133 | response, resources.MatchedBets, date_time_sent, date_time_received 134 | ) 135 | 136 | def get_positions(self, event_ids=None, market_ids=None, runner_ids=None, offset=0, per_page=500, session=None): 137 | #TODO: Make positions resource 138 | """ 139 | Get potential profit or loss on each runner. 140 | 141 | :param event_ids: operate only on orders on specified events. 142 | :type event_ids: comma separated string 143 | :param market_ids: operate only on orders on specified markets. 144 | :type market_ids: comma separated string 145 | :param runner_ids: operate only on orders on specified runners. 146 | :type runner_ids: comma separated string 147 | :param offset: starting point of results. Default 0. 148 | :type offset: int 149 | :param per_page: no. of offers returned in a single response, Max 500. Default 20. 150 | :type per_page: int 151 | :param session: requests session to be used. 152 | :type session: requests.Session 153 | :returns: Orders data 154 | :raises: MatchbookAPI.bin.exceptions.ApiError 155 | """ 156 | params = clean_locals(locals()) 157 | date_time_sent = datetime.datetime.utcnow() 158 | method = 'account/positions' 159 | response = self.request("GET", self.client.urn_edge, method, params=params, session=session) 160 | date_time_received = datetime.datetime.utcnow() 161 | return self.process_response( 162 | response.json().get('bets', []), resources.Order, date_time_sent, date_time_received 163 | ) 164 | 165 | def amend_orders(self, order_id, odds, side, stake, session=None): 166 | """ 167 | Adjust/Update an order(s), multiple orders can be adjusted by providing lists of the required arguments. 168 | 169 | :param order_id: order id to adjust. 170 | :type order_id: int 171 | :param odds: odds at which we wish to place the bet. 172 | :type odds: float 173 | :param side: back,lay|win,lose side to place bets on. 174 | :type side: MatchbookAPI.bin.enums.Side 175 | :param stake: amount in account currency to place on the bet. 176 | :type stake: float 177 | :param session: requests session to be used. 178 | :type session: requests.Session 179 | :returns: Orders responses, i.e. filled or at exchange or errors. 180 | :raises: MatchbookAPI.bin.exceptions.ApiError 181 | 182 | """ 183 | date_time_sent = datetime.datetime.utcnow() 184 | params = { 185 | 'offers': [], 186 | 'odds-type': self.client.odds_type, 187 | 'exchange-type': self.client.exchange_type, 188 | 'currency': self.client.currency, 189 | } 190 | if isinstance(order_id, list): 191 | method = 'offers' 192 | for i, _ in enumerate(order_id): 193 | params['offers'].append({'id': order_id[i], 'side': side[i], 'stake': stake[i], 'odds': odds[i]}) 194 | else: 195 | method = 'offers/{}'.format(order_id) 196 | del params['offers'] 197 | params['stake'] = stake 198 | params['odds'] = odds 199 | response = self.request('PUT', self.client.urn_edge, method, data=params, session=session) 200 | date_time_received = datetime.datetime.utcnow() 201 | return self.process_response( 202 | response.json().get('offers', response.json()), resources.Order, date_time_sent, date_time_received 203 | ) 204 | 205 | def delete_bulk_orders(self, event_ids=None, market_ids=None, runner_ids=None, offer_ids=None, session=None): 206 | """ 207 | Delete all orders which fit the argument filters. 208 | 209 | :param event_ids: bulk delete orders on specified events. 210 | :type event_ids: comma separated string 211 | :param market_ids: bulk delete orders on specified markets. 212 | :type market_ids: comma separated string 213 | :param runner_ids: bulk delete orders on specified runners. 214 | :type runner_ids: comma separated string 215 | :param offer_ids: delete specific order id. Max offerids in one delete request is 25 216 | :type offer_ids: comma separated string 217 | :param session: requests session to be used. 218 | :type session: requests.Session 219 | :returns: orders deletion report. 220 | :raises: MatchbookAPI.bin.exceptions.ApiError 221 | """ 222 | params = clean_locals(locals()) 223 | date_time_sent = datetime.datetime.utcnow() 224 | method = 'offers' 225 | response = self.request('DELETE', self.client.urn_edge, method, data=params, session=session) 226 | date_time_received = datetime.datetime.utcnow() 227 | return self.process_response( 228 | response.json().get('offers', []), resources.Order, date_time_sent, date_time_received 229 | ) 230 | 231 | def delete_order(self, offer_id, session=None): 232 | """ 233 | Delete all orders which fit the argument filters. 234 | 235 | :param offer_id: delete specific order id. 236 | :type offer_id: int 237 | :param session: requests session to be used. 238 | :type session: requests.Session 239 | :returns: order deletion report. 240 | :raises: MatchbookAPI.bin.exceptions.ApiError 241 | 242 | """ 243 | date_time_sent = datetime.datetime.utcnow() 244 | method = 'offers/{}'.format(offer_id) 245 | response = self.request('DELETE', self.client.urn_edge, method, session=session) 246 | date_time_received = datetime.datetime.utcnow() 247 | return self.process_response( 248 | response.json(), resources.Order, date_time_sent, date_time_received 249 | ) 250 | -------------------------------------------------------------------------------- /matchbook/tests/resources/referencedata_countries.json: -------------------------------------------------------------------------------- 1 | {'countries': [{'country-code': 'AF', 'country-id': 77, 'name': 'Afghanistan'}, 2 | {'country-code': 'AL', 'country-id': 78, 'name': 'Albania'}, 3 | {'country-code': 'ALD', 'country-id': 76, 'name': 'Alderney'}, 4 | {'country-code': 'DZ', 'country-id': 79, 'name': 'Algeria'}, 5 | {'country-code': 'AD', 'country-id': 80, 'name': 'Andorra'}, 6 | {'country-code': 'AO', 'country-id': 81, 'name': 'Angola'}, 7 | {'country-code': 'AG', 'country-id': 45, 'name': 'Antigua and Barbuda'}, 8 | {'country-code': 'AR', 'country-id': 20, 'name': 'Argentina'}, 9 | {'country-code': 'AM', 'country-id': 82, 'name': 'Armenia'}, 10 | {'country-code': 'AU', 'country-id': 18, 'name': 'Australia'}, 11 | {'country-code': 'AT', 'country-id': 24, 'name': 'Austria'}, 12 | {'country-code': 'AZ', 'country-id': 83, 'name': 'Azerbaijan'}, 13 | {'country-code': 'BH', 'country-id': 84, 'name': 'Bahrain'}, 14 | {'country-code': 'BD', 'country-id': 85, 'name': 'Bangladesh'}, 15 | {'country-code': 'BB', 'country-id': 47, 'name': 'Barbados'}, 16 | {'country-code': 'BY', 'country-id': 86, 'name': 'Belarus'}, 17 | {'country-code': 'BE', 'country-id': 22, 'name': 'Belgium'}, 18 | {'country-code': 'BZ', 'country-id': 48, 'name': 'Belize'}, 19 | {'country-code': 'BJ', 'country-id': 87, 'name': 'Benin'}, 20 | {'country-code': 'BT', 'country-id': 88, 'name': 'Bhutan'}, 21 | {'country-code': 'BO', 'country-id': 71, 'name': 'Bolivia'}, 22 | {'country-code': 'BA', 'country-id': 89, 'name': 'Bosnia and Herzegovina'}, 23 | {'country-code': 'BW', 'country-id': 90, 'name': 'Botswana'}, 24 | {'country-code': 'BR', 'country-id': 13, 'name': 'Brazil'}, 25 | {'country-code': 'BN', 'country-id': 91, 'name': 'Brunei'}, 26 | {'country-code': 'BG', 'country-id': 92, 'name': 'Bulgaria'}, 27 | {'country-code': 'BF', 'country-id': 93, 'name': 'Burkina Faso'}, 28 | {'country-code': 'BUR', 'country-id': 94, 'name': 'Burma'}, 29 | {'country-code': 'BI', 'country-id': 95, 'name': 'Burundi'}, 30 | {'country-code': 'KH', 'country-id': 96, 'name': 'Cambodia'}, 31 | {'country-code': 'CM', 'country-id': 97, 'name': 'Cameroon'}, 32 | {'country-code': 'CV', 'country-id': 98, 'name': 'Cape Verde'}, 33 | {'country-code': 'CF', 'country-id': 99, 'name': 'Central African Republic'}, 34 | {'country-code': 'TD', 'country-id': 100, 'name': 'Chad'}, 35 | {'country-code': 'CL', 'country-id': 75, 'name': 'Chile'}, 36 | {'country-code': 'CN', 'country-id': 10, 'name': 'China'}, 37 | {'country-code': 'CO', 'country-id': 60, 'name': 'Colombia'}, 38 | {'country-code': 'KM', 'country-id': 101, 'name': 'Comoros'}, 39 | {'country-code': 'CD', 40 | 'country-id': 102, 41 | 'name': 'Congo, Democratic Republic of'}, 42 | {'country-code': 'CG', 'country-id': 103, 'name': 'Congo, Republic of'}, 43 | {'country-code': 'CR', 'country-id': 50, 'name': 'Costa Rica'}, 44 | {'country-code': 'CI', 'country-id': 104, 'name': 'Cote d Ivoire'}, 45 | {'country-code': 'HR', 'country-id': 63, 'name': 'Croatia'}, 46 | {'country-code': 'CU', 'country-id': 51, 'name': 'Cuba'}, 47 | {'country-code': 'CW', 'country-id': 52, 'name': 'Curacao'}, 48 | {'country-code': 'CY', 'country-id': 105, 'name': 'Cyprus'}, 49 | {'country-code': 'CZ', 'country-id': 39, 'name': 'Czech Republic'}, 50 | {'country-code': 'DK', 'country-id': 41, 'name': 'Denmark'}, 51 | {'country-code': 'DJ', 'country-id': 106, 'name': 'Djibouti'}, 52 | {'country-code': 'DM', 'country-id': 107, 'name': 'Dominica'}, 53 | {'country-code': 'DO', 'country-id': 53, 'name': 'Dominican Republic'}, 54 | {'country-code': 'EC', 'country-id': 108, 'name': 'Ecuador'}, 55 | {'country-code': 'EG', 'country-id': 36, 'name': 'Egypt'}, 56 | {'country-code': 'SV', 'country-id': 109, 'name': 'El Salvador'}, 57 | {'country-code': 'GQ', 'country-id': 110, 'name': 'Equatorial Guinea'}, 58 | {'country-code': 'ER', 'country-id': 111, 'name': 'Eritrea'}, 59 | {'country-code': 'EE', 'country-id': 62, 'name': 'Estonia'}, 60 | {'country-code': 'ET', 'country-id': 70, 'name': 'Ethiopia'}, 61 | {'country-code': 'FJ', 'country-id': 112, 'name': 'Fiji'}, 62 | {'country-code': 'FI', 'country-id': 30, 'name': 'Finland'}, 63 | {'country-code': 'GA', 'country-id': 113, 'name': 'Gabon'}, 64 | {'country-code': 'GM', 'country-id': 114, 'name': 'Gambia'}, 65 | {'country-code': 'GE', 'country-id': 115, 'name': 'Georgia'}, 66 | {'country-code': 'DE', 'country-id': 3, 'name': 'Germany'}, 67 | {'country-code': 'GH', 'country-id': 116, 'name': 'Ghana'}, 68 | {'country-code': 'GI', 'country-id': 215, 'name': 'Gibraltar'}, 69 | {'country-code': 'GR', 'country-id': 31, 'name': 'Greece'}, 70 | {'country-code': 'GD', 'country-id': 117, 'name': 'Grenada'}, 71 | {'country-code': 'GT', 'country-id': 118, 'name': 'Guatemala'}, 72 | {'country-code': 'GG', 'country-id': 213, 'name': 'Guernsey'}, 73 | {'country-code': 'GN', 'country-id': 119, 'name': 'Guinea'}, 74 | {'country-code': 'GW', 'country-id': 120, 'name': 'Guinea-Bissau'}, 75 | {'country-code': 'GY', 'country-id': 121, 'name': 'Guyana'}, 76 | {'country-code': 'HT', 'country-id': 122, 'name': 'Haiti'}, 77 | {'country-code': 'VA', 'country-id': 123, 'name': 'Holy See'}, 78 | {'country-code': 'HN', 'country-id': 124, 'name': 'Honduras'}, 79 | {'country-code': 'HK', 'country-id': 42, 'name': 'Hong Kong'}, 80 | {'country-code': 'HU', 'country-id': 40, 'name': 'Hungary'}, 81 | {'country-code': 'IS', 'country-id': 125, 'name': 'Iceland'}, 82 | {'country-code': 'IN', 'country-id': 15, 'name': 'India'}, 83 | {'country-code': 'ID', 'country-id': 126, 'name': 'Indonesia'}, 84 | {'country-code': 'IR', 'country-id': 127, 'name': 'Iran'}, 85 | {'country-code': 'IQ', 'country-id': 128, 'name': 'Iraq'}, 86 | {'country-code': 'IE', 'country-id': 35, 'name': 'Ireland'}, 87 | {'country-code': 'IM', 'country-id': 214, 'name': 'Isle of Man'}, 88 | {'country-code': 'IL', 'country-id': 34, 'name': 'Israel'}, 89 | {'country-code': 'JM', 'country-id': 54, 'name': 'Jamaica'}, 90 | {'country-code': 'JP', 'country-id': 2, 'name': 'Japan'}, 91 | {'country-code': 'JE', 'country-id': 216, 'name': 'Jersey'}, 92 | {'country-code': 'JO', 'country-id': 129, 'name': 'Jordan'}, 93 | {'country-code': 'KZ', 'country-id': 130, 'name': 'Kazakhstan'}, 94 | {'country-code': 'KE', 'country-id': 131, 'name': 'Kenya'}, 95 | {'country-code': 'KI', 'country-id': 132, 'name': 'Kiribati'}, 96 | {'country-code': 'KP', 'country-id': 133, 'name': 'Korea North'}, 97 | {'country-code': 'KV', 'country-id': 134, 'name': 'Kosovo'}, 98 | {'country-code': 'KW', 'country-id': 135, 'name': 'Kuwait'}, 99 | {'country-code': 'KG', 'country-id': 136, 'name': 'Kyrgyzstan'}, 100 | {'country-code': 'LA', 'country-id': 137, 'name': 'Laos'}, 101 | {'country-code': 'LV', 'country-id': 138, 'name': 'Latvia'}, 102 | {'country-code': 'LB', 'country-id': 139, 'name': 'Lebanon'}, 103 | {'country-code': 'LS', 'country-id': 140, 'name': 'Lesotho'}, 104 | {'country-code': 'LR', 'country-id': 141, 'name': 'Liberia'}, 105 | {'country-code': 'LY', 'country-id': 142, 'name': 'Libya'}, 106 | {'country-code': 'LI', 'country-id': 143, 'name': 'Liechtenstein'}, 107 | {'country-code': 'LT', 'country-id': 144, 'name': 'Lithuania'}, 108 | {'country-code': 'LU', 'country-id': 145, 'name': 'Luxembourg'}, 109 | {'country-code': 'MO', 'country-id': 212, 'name': 'Macau'}, 110 | {'country-code': 'MK', 'country-id': 146, 'name': 'Macedonia'}, 111 | {'country-code': 'MG', 'country-id': 147, 'name': 'Madagascar'}, 112 | {'country-code': 'MW', 'country-id': 148, 'name': 'Malawi'}, 113 | {'country-code': 'MY', 'country-id': 37, 'name': 'Malaysia'}, 114 | {'country-code': 'MV', 'country-id': 149, 'name': 'Maldives'}, 115 | {'country-code': 'ML', 'country-id': 150, 'name': 'Mali'}, 116 | {'country-code': 'MT', 'country-id': 67, 'name': 'Malta'}, 117 | {'country-code': 'MH', 'country-id': 151, 'name': 'Marshall Islands'}, 118 | {'country-code': 'MR', 'country-id': 152, 'name': 'Mauritania'}, 119 | {'country-code': 'MU', 'country-id': 65, 'name': 'Mauritius'}, 120 | {'country-code': 'MX', 'country-id': 14, 'name': 'Mexico'}, 121 | {'country-code': 'FM', 122 | 'country-id': 153, 123 | 'name': 'Micronesia, Federated States of'}, 124 | {'country-code': 'MD', 'country-id': 154, 'name': 'Moldova'}, 125 | {'country-code': 'MC', 'country-id': 64, 'name': 'Monaco'}, 126 | {'country-code': 'MN', 'country-id': 155, 'name': 'Mongolia'}, 127 | {'country-code': 'ME', 'country-id': 156, 'name': 'Montenegro'}, 128 | {'country-code': 'MA', 'country-id': 157, 'name': 'Morocco'}, 129 | {'country-code': 'MZ', 'country-id': 158, 'name': 'Mozambique'}, 130 | {'country-code': 'NA', 'country-id': 159, 'name': 'Namibia'}, 131 | {'country-code': 'NR', 'country-id': 160, 'name': 'Nauru'}, 132 | {'country-code': 'NP', 'country-id': 161, 'name': 'Nepal'}, 133 | {'country-code': 'NL', 'country-id': 17, 'name': 'Netherlands'}, 134 | {'country-code': 'NZ', 'country-id': 38, 'name': 'New Zealand'}, 135 | {'country-code': 'NI', 'country-id': 162, 'name': 'Nicaragua'}, 136 | {'country-code': 'NE', 'country-id': 163, 'name': 'Niger'}, 137 | {'country-code': 'NG', 'country-id': 164, 'name': 'Nigeria'}, 138 | {'country-code': 'NO', 'country-id': 27, 'name': 'Norway'}, 139 | {'country-code': 'OM', 'country-id': 165, 'name': 'Oman'}, 140 | {'country-code': 'PK', 'country-id': 166, 'name': 'Pakistan'}, 141 | {'country-code': 'PW', 'country-id': 167, 'name': 'Palau'}, 142 | {'country-code': 'PA', 'country-id': 68, 'name': 'Panama'}, 143 | {'country-code': 'PG', 'country-id': 168, 'name': 'Papua New Guinea'}, 144 | {'country-code': 'PY', 'country-id': 169, 'name': 'Paraguay'}, 145 | {'country-code': 'PE', 'country-id': 170, 'name': 'Peru'}, 146 | {'country-code': 'PH', 'country-id': 171, 'name': 'Philippines'}, 147 | {'country-code': 'PL', 'country-id': 26, 'name': 'Poland'}, 148 | {'country-code': 'PR', 'country-id': 55, 'name': 'Puerto Rico'}, 149 | {'country-code': 'QA', 'country-id': 172, 'name': 'Qatar'}, 150 | {'country-code': 'RO', 'country-id': 74, 'name': 'Romania'}, 151 | {'country-code': 'RU', 'country-id': 25, 'name': 'Russia'}, 152 | {'country-code': 'RW', 'country-id': 174, 'name': 'Rwanda'}, 153 | {'country-code': 'LC', 'country-id': 175, 'name': 'Saint Lucia'}, 154 | {'country-code': 'VC', 155 | 'country-id': 176, 156 | 'name': 'Saint Vincent and the Grenadines'}, 157 | {'country-code': 'WS', 'country-id': 177, 'name': 'Samoa'}, 158 | {'country-code': 'SM', 'country-id': 178, 'name': 'San Marino'}, 159 | {'country-code': 'ST', 'country-id': 179, 'name': 'Sao Tome and Principe'}, 160 | {'country-code': 'SA', 'country-id': 28, 'name': 'Saudi Arabia'}, 161 | {'country-code': 'SN', 'country-id': 180, 'name': 'Senegal'}, 162 | {'country-code': 'RS', 'country-id': 66, 'name': 'Serbia'}, 163 | {'country-code': 'SC', 'country-id': 181, 'name': 'Seychelles'}, 164 | {'country-code': 'SL', 'country-id': 182, 'name': 'Sierra Leone'}, 165 | {'country-code': 'SG', 'country-id': 43, 'name': 'Singapore'}, 166 | {'country-code': 'SK', 'country-id': 183, 'name': 'Slovakia'}, 167 | {'country-code': 'SI', 'country-id': 69, 'name': 'Slovenia'}, 168 | {'country-code': 'SB', 'country-id': 184, 'name': 'Solomon Islands'}, 169 | {'country-code': 'SO', 'country-id': 185, 'name': 'Somalia'}, 170 | {'country-code': 'ZA', 'country-id': 29, 'name': 'South Africa'}, 171 | {'country-code': 'KR', 'country-id': 16, 'name': 'South Korea'}, 172 | {'country-code': 'SS', 'country-id': 186, 'name': 'South Sudan'}, 173 | {'country-code': 'LK', 'country-id': 187, 'name': 'Sri Lanka'}, 174 | {'country-code': 'KN', 'country-id': 56, 'name': 'St. Kitts and Nevis'}, 175 | {'country-code': 'SD', 'country-id': 188, 'name': 'Sudan'}, 176 | {'country-code': 'SR', 'country-id': 189, 'name': 'Suriname'}, 177 | {'country-code': 'SZ', 'country-id': 190, 'name': 'Swaziland'}, 178 | {'country-code': 'SE', 'country-id': 23, 'name': 'Sweden'}, 179 | {'country-code': 'CH', 'country-id': 21, 'name': 'Switzerland'}, 180 | {'country-code': 'SY', 'country-id': 191, 'name': 'Syria'}, 181 | {'country-code': 'TW', 'country-id': 19, 'name': 'Taiwan'}, 182 | {'country-code': 'TJ', 'country-id': 192, 'name': 'Tajikistan'}, 183 | {'country-code': 'TZ', 'country-id': 193, 'name': 'Tanzania'}, 184 | {'country-code': 'TH', 'country-id': 32, 'name': 'Thailand'}, 185 | {'country-code': 'TL', 'country-id': 194, 'name': 'Timor-Leste'}, 186 | {'country-code': 'TG', 'country-id': 195, 'name': 'Togo'}, 187 | {'country-code': 'TO', 'country-id': 196, 'name': 'Tonga'}, 188 | {'country-code': 'TT', 'country-id': 57, 'name': 'Trinidad and Tobago'}, 189 | {'country-code': 'TN', 'country-id': 197, 'name': 'Tunisia'}, 190 | {'country-code': 'TR', 'country-id': 73, 'name': 'Turkey'}, 191 | {'country-code': 'TM', 'country-id': 199, 'name': 'Turkmenistan'}, 192 | {'country-code': 'TV', 'country-id': 200, 'name': 'Tuvalu'}, 193 | {'country-code': 'UG', 'country-id': 201, 'name': 'Uganda'}, 194 | {'country-code': 'UA', 'country-id': 72, 'name': 'Ukraine'}, 195 | {'country-code': 'AE', 'country-id': 203, 'name': 'United Arab Emirates'}, 196 | {'country-code': 'GB', 'country-id': 204, 'name': 'United Kingdom'}, 197 | {'country-code': 'UY', 'country-id': 205, 'name': 'Uruguay'}, 198 | {'country-code': 'UZ', 'country-id': 206, 'name': 'Uzbekistan'}, 199 | {'country-code': 'VU', 'country-id': 207, 'name': 'Vanuatu'}, 200 | {'country-code': 'VE', 'country-id': 208, 'name': 'Venezuela'}, 201 | {'country-code': 'VN', 'country-id': 61, 'name': 'Vietnam'}, 202 | {'country-code': 'VG', 'country-id': 58, 'name': 'Virgin Islands (British)'}, 203 | {'country-code': 'YE', 'country-id': 209, 'name': 'Yemen'}, 204 | {'country-code': 'ZM', 'country-id': 210, 'name': 'Zambia'}, 205 | {'country-code': 'ZW', 'country-id': 211, 'name': 'Zimbabwe'}], 206 | 'total': 199} -------------------------------------------------------------------------------- /matchbook/tests/resources/referencedata_navigation.json: -------------------------------------------------------------------------------- 1 | [{'id': 1, 2 | 'meta-tags': [{'id': 9, 3 | 'meta-tags': [{'id': 553671759780032, 4 | 'meta-tags': [{'id': 297063445660036, 5 | 'meta-tags': [], 6 | 'name': 'R1', 7 | 'type': 'DATE', 8 | 'url-name': 'r1'}], 9 | 'name': 'ATP Washington', 10 | 'type': 'COMPETITION', 11 | 'url-name': 'atp-washington'}, 12 | {'id': 553768186380032, 13 | 'meta-tags': [{'id': 297063445660036, 14 | 'meta-tags': [], 15 | 'name': 'R1', 16 | 'type': 'DATE', 17 | 'url-name': 'r1'}], 18 | 'name': 'WTA Washington', 19 | 'type': 'COMPETITION', 20 | 'url-name': 'wta-washington'}, 21 | {'id': 553786658080032, 22 | 'meta-tags': [{'id': 297063445660036, 23 | 'meta-tags': [], 24 | 'name': 'R1', 25 | 'type': 'DATE', 26 | 'url-name': 'r1'}], 27 | 'name': 'WTA Stanford', 28 | 'type': 'COMPETITION', 29 | 'url-name': 'wta-stanford'}, 30 | {'id': 553198426140009, 31 | 'meta-tags': [{'id': 297063445660036, 32 | 'meta-tags': [], 33 | 'name': 'R1', 34 | 'type': 'DATE', 35 | 'url-name': 'r1'}, 36 | {'id': 291015339500036, 37 | 'meta-tags': [], 38 | 'name': 'R16', 39 | 'type': 'DATE', 40 | 'url-name': 'r16'}], 41 | 'name': 'ATP Kitzbuhel', 42 | 'type': 'COMPETITION', 43 | 'url-name': 'atp-kitzbuhel'}, 44 | {'id': 553967050400032, 45 | 'meta-tags': [{'id': 297063445660036, 46 | 'meta-tags': [], 47 | 'name': 'R1', 48 | 'type': 'DATE', 49 | 'url-name': 'r1'}], 50 | 'name': 'ATP Los Cabos', 51 | 'type': 'COMPETITION', 52 | 'url-name': 'atp-los-cabos'}, 53 | {'id': 555059592330032, 54 | 'meta-tags': [{'id': 297063445660036, 55 | 'meta-tags': [], 56 | 'name': 'R1', 57 | 'type': 'DATE', 58 | 'url-name': 'r1'}], 59 | 'name': 'ATP Challenger Lexington', 60 | 'type': 'COMPETITION', 61 | 'url-name': 'atp-challenger-lexington'}, 62 | {'id': 555499938160009, 63 | 'meta-tags': [{'id': 297063445660036, 64 | 'meta-tags': [], 65 | 'name': 'R1', 66 | 'type': 'DATE', 67 | 'url-name': 'r1'}], 68 | 'name': 'ATP Washington Doubles', 69 | 'type': 'COMPETITION', 70 | 'url-name': 'atp-washington-doubles'}, 71 | {'id': 555679454040032, 72 | 'meta-tags': [{'id': 297063445660036, 73 | 'meta-tags': [], 74 | 'name': 'R1', 75 | 'type': 'DATE', 76 | 'url-name': 'r1'}], 77 | 'name': 'ATP Los Cabos Doubles', 78 | 'type': 'COMPETITION', 79 | 'url-name': 'atp-los-cabos-doubles'}], 80 | 'name': 'Tennis', 81 | 'type': 'SPORT', 82 | 'url-name': 'tennis'}, 83 | {'id': 24735152712200, 84 | 'meta-tags': [{'id': 24751810709200, 85 | 'meta-tags': [{'id': 176072200410023, 86 | 'meta-tags': [], 87 | 'name': 'Goodwood', 88 | 'type': 'LOCATION', 89 | 'url-name': 'Goodwood'}], 90 | 'name': 'Ante Post', 91 | 'type': 'COUNTRY', 92 | 'url-name': 'ante-post'}, 93 | {'id': 10812638253700, 94 | 'meta-tags': [{'id': 176072200410023, 95 | 'meta-tags': [], 96 | 'name': 'Goodwood', 97 | 'type': 'LOCATION', 98 | 'url-name': 'Goodwood'}], 99 | 'name': 'UK', 100 | 'type': 'COUNTRY', 101 | 'url-name': 'uk'}], 102 | 'name': 'Horse Racing', 103 | 'type': 'SPORT', 104 | 'url-name': 'horse-racing'}, 105 | {'id': 1, 106 | 'meta-tags': [{'id': 491503123380010, 107 | 'meta-tags': [{'id': 498612642910010, 108 | 'meta-tags': [], 109 | 'name': 'September 10th 2017', 110 | 'type': 'DATE', 111 | 'url-name': 'september-10th-2017'}, 112 | {'id': 498613549250010, 113 | 'meta-tags': [], 114 | 'name': 'September 11th 2017', 115 | 'type': 'DATE', 116 | 'url-name': 'september-11th-2017'}, 117 | {'id': 499004730550010, 118 | 'meta-tags': [], 119 | 'name': 'September 7th 2017', 120 | 'type': 'DATE', 121 | 'url-name': 'september-7th-2017'}], 122 | 'name': 'NFL', 123 | 'type': 'COMPETITION', 124 | 'url-name': 'nfl'}, 125 | {'id': 519718525730009, 126 | 'meta-tags': [{'id': 547744278460009, 127 | 'meta-tags': [], 128 | 'name': 'August 5th 2017', 129 | 'type': 'DATE', 130 | 'url-name': 'august-5th-2017'}, 131 | {'id': 547114547470032, 132 | 'meta-tags': [], 133 | 'name': 'August 4th 2017', 134 | 'type': 'DATE', 135 | 'url-name': 'august-4th-2017'}, 136 | {'id': 551626692040009, 137 | 'meta-tags': [], 138 | 'name': 'August 3rd 2017', 139 | 'type': 'DATE', 140 | 'url-name': 'august-3rd-2017'}], 141 | 'name': 'CFL', 142 | 'type': 'COMPETITION', 143 | 'url-name': 'cfl'}], 144 | 'name': 'American Football', 145 | 'type': 'SPORT', 146 | 'url-name': 'american-football'}, 147 | {'id': 3, 148 | 'meta-tags': [{'id': 451097830090009, 149 | 'meta-tags': [{'id': 451081871520010, 150 | 'meta-tags': [{'id': 548851381700032, 151 | 'meta-tags': [], 152 | 'name': 'July 31st 2017', 153 | 'type': 'DATE', 154 | 'url-name': 'july-31st-2017'}], 155 | 'name': 'NL Games', 156 | 'type': 'OTHER', 157 | 'url-name': 'nl-games'}, 158 | {'id': 474008087050009, 159 | 'meta-tags': [{'id': 548851381700032, 160 | 'meta-tags': [], 161 | 'name': 'July 31st 2017', 162 | 'type': 'DATE', 163 | 'url-name': 'july-31st-2017'}], 164 | 'name': 'AL Games', 165 | 'type': 'OTHER', 166 | 'url-name': 'al-games'}, 167 | {'id': 473790367990010, 168 | 'meta-tags': [{'id': 548851381700032, 169 | 'meta-tags': [], 170 | 'name': 'July 31st 2017', 171 | 'type': 'DATE', 172 | 'url-name': 'july-31st-2017'}], 173 | 'name': 'IL Games', 174 | 'type': 'OTHER', 175 | 'url-name': 'il-games'}], 176 | 'name': 'MLB', 177 | 'type': 'COMPETITION', 178 | 'url-name': 'mlb'}, 179 | {'id': 449716732180009, 180 | 'meta-tags': [{'id': 551190350810032, 181 | 'meta-tags': [], 182 | 'name': 'August 1st 2017', 183 | 'type': 'DATE', 184 | 'url-name': 'august-1st-2017'}], 185 | 'name': 'NPB', 186 | 'type': 'COMPETITION', 187 | 'url-name': 'NPB'}], 188 | 'name': 'Baseball', 189 | 'type': 'SPORT', 190 | 'url-name': 'baseball'}, 191 | {'id': 110, 192 | 'meta-tags': [{'id': 337788021510021, 193 | 'meta-tags': [], 194 | 'name': 'Test Series', 195 | 'type': 'COMPETITION', 196 | 'url-name': 'test-series'}], 197 | 'name': 'Cricket', 198 | 'type': 'SPORT', 199 | 'url-name': 'cricket'}, 200 | {'id': 118, 201 | 'meta-tags': [{'id': 248897962350020, 202 | 'meta-tags': [{'id': 548850504600032, 203 | 'meta-tags': [], 204 | 'name': 'August 6th 2017', 205 | 'type': 'DATE', 206 | 'url-name': 'august-6th-2017'}, 207 | {'id': 542770377450009, 208 | 'meta-tags': [], 209 | 'name': 'August 13th 2017', 210 | 'type': 'DATE', 211 | 'url-name': 'august-13th-2017'}], 212 | 'name': 'Senior Hurling Championship', 213 | 'type': 'COMPETITION', 214 | 'url-name': 'senior-hurling-championship'}], 215 | 'name': 'Hurling', 216 | 'type': 'SPORT', 217 | 'url-name': 'hurling'}, 218 | {'id': 8, 219 | 'meta-tags': [{'id': 555487785820032, 220 | 'meta-tags': [{'id': 489003593880009, 221 | 'meta-tags': [], 222 | 'name': 'Outright Winner', 223 | 'type': 'OTHER', 224 | 'url-name': 'outright-winner'}], 225 | 'name': 'WGC Bridgestone Invitational 2017', 226 | 'type': 'COMPETITION', 227 | 'url-name': 'wgc-bridgestone-invitational-2017'}, 228 | {'id': 550353902480032, 229 | 'meta-tags': [{'id': 489003593880009, 230 | 'meta-tags': [], 231 | 'name': 'Outright Winner', 232 | 'type': 'OTHER', 233 | 'url-name': 'outright-winner'}], 234 | 'name': 'US PGA Championship 2017', 235 | 'type': 'COMPETITION', 236 | 'url-name': 'us-pga-championship-2017'}], 237 | 'name': 'Golf', 238 | 'type': 'SPORT', 239 | 'url-name': 'golf'}, 240 | {'id': 112, 241 | 'meta-tags': [{'id': 440496918480010, 242 | 'meta-tags': [{'id': 547114547470032, 243 | 'meta-tags': [], 244 | 'name': 'August 4th 2017', 245 | 'type': 'DATE', 246 | 'url-name': 'august-4th-2017'}, 247 | {'id': 547744278460009, 248 | 'meta-tags': [], 249 | 'name': 'August 5th 2017', 250 | 'type': 'DATE', 251 | 'url-name': 'august-5th-2017'}, 252 | {'id': 548850504600032, 253 | 'meta-tags': [], 254 | 'name': 'August 6th 2017', 255 | 'type': 'DATE', 256 | 'url-name': 'august-6th-2017'}], 257 | 'name': 'AFL', 258 | 'type': 'COMPETITION', 259 | 'url-name': 'afl'}], 260 | 'name': 'Australian Rules', 261 | 'type': 'SPORT', 262 | 'url-name': 'australian-rules'}, 263 | {'id': 123, 264 | 'meta-tags': [{'id': 537416285300032, 265 | 'meta-tags': [{'id': 537416470420009, 266 | 'meta-tags': [{'id': 551626703180032, 267 | 'meta-tags': [], 268 | 'name': 'August 2nd 2017', 269 | 'type': 'DATE', 270 | 'url-name': 'august-2nd-2017'}, 271 | {'id': 551190350810032, 272 | 'meta-tags': [], 273 | 'name': 'August 1st 2017', 274 | 'type': 'DATE', 275 | 'url-name': 'august-1st-2017'}], 276 | 'name': 'Champions Korea', 277 | 'type': 'OTHER', 278 | 'url-name': 'champions-korea'}], 279 | 'name': 'League of Legends', 280 | 'type': 'COMPETITION', 281 | 'url-name': 'league-of-legends'}], 282 | 'name': 'eSports', 283 | 'type': 'SPORT', 284 | 'url-name': 'esports'}, 285 | {'id': 117, 286 | 'meta-tags': [{'id': 493423584740009, 287 | 'meta-tags': [{'id': 547744278460009, 288 | 'meta-tags': [], 289 | 'name': 'August 5th 2017', 290 | 'type': 'DATE', 291 | 'url-name': 'august-5th-2017'}, 292 | {'id': 555374548810032, 293 | 'meta-tags': [], 294 | 'name': 'August 7th 2017', 295 | 'type': 'DATE', 296 | 'url-name': 'august-7th-2017'}], 297 | 'name': 'Senior Football Championship', 298 | 'type': 'COMPETITION', 299 | 'url-name': 'senior-football-championship'}], 300 | 'name': 'Gaelic Football', 301 | 'type': 'SPORT', 302 | 'url-name': 'gaelic-football'}, 303 | {'id': 18, 304 | 'meta-tags': [{'id': 415403681110009, 305 | 'meta-tags': [{'id': 547744278460009, 306 | 'meta-tags': [], 307 | 'name': 'August 5th 2017', 308 | 'type': 'DATE', 309 | 'url-name': 'august-5th-2017'}], 310 | 'name': 'Super Rugby', 311 | 'type': 'COMPETITION', 312 | 'url-name': 'super-rugby'}], 313 | 'name': 'Rugby Union', 314 | 'type': 'SPORT', 315 | 'url-name': 'rugby-union'}, 316 | {'id': 114, 317 | 'meta-tags': [{'id': 419805790450009, 318 | 'meta-tags': [{'id': 551626692040009, 319 | 'meta-tags': [], 320 | 'name': 'August 3rd 2017', 321 | 'type': 'DATE', 322 | 'url-name': 'august-3rd-2017'}, 323 | {'id': 547114547470032, 324 | 'meta-tags': [], 325 | 'name': 'August 4th 2017', 326 | 'type': 'DATE', 327 | 'url-name': 'august-4th-2017'}, 328 | {'id': 547744278460009, 329 | 'meta-tags': [], 330 | 'name': 'August 5th 2017', 331 | 'type': 'DATE', 332 | 'url-name': 'august-5th-2017'}, 333 | {'id': 548850504600032, 334 | 'meta-tags': [], 335 | 'name': 'August 6th 2017', 336 | 'type': 'DATE', 337 | 'url-name': 'august-6th-2017'}], 338 | 'name': 'NRL', 339 | 'type': 'COMPETITION', 340 | 'url-name': 'nrl'}], 341 | 'name': 'Rugby League', 342 | 'type': 'SPORT', 343 | 'url-name': 'rugby-league'}, 344 | {'id': 385227477790005, 345 | 'meta-tags': [{'id': 384790875060031, 346 | 'meta-tags': [{'id': 384791656430005, 347 | 'meta-tags': [], 348 | 'name': 'Trump Exit 2017', 349 | 'type': 'COMPETITION', 350 | 'url-name': 'trump-exit-2017'}], 351 | 'name': 'US Politics', 352 | 'type': 'COUNTRY', 353 | 'url-name': 'us-politics'}, 354 | {'id': 466430057300010, 355 | 'meta-tags': [{'id': 480405994480010, 356 | 'meta-tags': [{'id': 466502671620009, 357 | 'meta-tags': [], 358 | 'name': 'Most Seats', 359 | 'type': 'COMPETITION', 360 | 'url-name': 'most-seats'}], 361 | 'name': 'Next General Election', 362 | 'type': 'OTHER', 363 | 'url-name': 'next-general-election'}], 364 | 'name': 'UK Politics', 365 | 'type': 'COUNTRY', 366 | 'url-name': 'uk-politics'}, 367 | {'id': 482889536080010, 368 | 'meta-tags': [{'id': 482889827010009, 369 | 'meta-tags': [], 370 | 'name': 'Next Chancellor', 371 | 'type': 'COMPETITION', 372 | 'url-name': 'next-chancellor'}], 373 | 'name': 'German Politics', 374 | 'type': 'COUNTRY', 375 | 'url-name': 'german-politics'}, 376 | {'id': 466430057300010, 377 | 'meta-tags': [{'id': 514988919260009, 378 | 'meta-tags': [], 379 | 'name': 'Second General Election in 2017', 380 | 'type': 'COMPETITION', 381 | 'url-name': 'second-general-election-in-2017'}], 382 | 'name': 'UK Politics', 383 | 'type': 'COUNTRY', 384 | 'url-name': 'uk-politics'}, 385 | {'id': 514993464580009, 386 | 'meta-tags': [{'id': 514979929120010, 387 | 'meta-tags': [], 388 | 'name': 'Catalan Independence Referendum', 389 | 'type': 'COMPETITION', 390 | 'url-name': 'catalan-independence-referendum'}], 391 | 'name': 'Spain Politics', 392 | 'type': 'COUNTRY', 393 | 'url-name': 'spain-politics'}], 394 | 'name': 'Politics', 395 | 'type': 'SPORT', 396 | 'url-name': 'politics'}, 397 | {'id': 14, 398 | 'meta-tags': [{'id': 411287773430010, 399 | 'meta-tags': [{'id': 484618172410010, 400 | 'meta-tags': [], 401 | 'name': 'September 16th 2017', 402 | 'type': 'DATE', 403 | 'url-name': 'september-16th-2017'}], 404 | 'name': 'Middleweight', 405 | 'type': 'COMPETITION', 406 | 'url-name': 'middleweight'}, 407 | {'id': 411286652840009, 408 | 'meta-tags': [{'id': 515317570390009, 409 | 'meta-tags': [], 410 | 'name': 'August 26th 2017', 411 | 'type': 'DATE', 412 | 'url-name': 'august-26th-2017'}], 413 | 'name': 'Super Welterweight', 414 | 'type': 'COMPETITION', 415 | 'url-name': 'super-welterweight'}, 416 | {'id': 410479509900010, 417 | 'meta-tags': [{'id': 537475064650032, 418 | 'meta-tags': [], 419 | 'name': 'September 23rd 2017', 420 | 'type': 'DATE', 421 | 'url-name': 'september-23rd-2017'}], 422 | 'name': 'Heavyweight', 423 | 'type': 'COMPETITION', 424 | 'url-name': 'heavyweight'}, 425 | {'id': 421455897460010, 426 | 'meta-tags': [{'id': 484618172410010, 427 | 'meta-tags': [], 428 | 'name': 'September 16th 2017', 429 | 'type': 'DATE', 430 | 'url-name': 'september-16th-2017'}], 431 | 'name': 'Super Middleweight', 432 | 'type': 'COMPETITION', 433 | 'url-name': 'super-middleweight'}, 434 | {'id': 551309148450009, 435 | 'meta-tags': [{'id': 547780661430032, 436 | 'meta-tags': [], 437 | 'name': 'August 19th 2017', 438 | 'type': 'DATE', 439 | 'url-name': 'august-19th-2017'}], 440 | 'name': 'Light Welterweight', 441 | 'type': 'COMPETITION', 442 | 'url-name': 'light-welterweight'}], 443 | 'name': 'Boxing', 444 | 'type': 'SPORT', 445 | 'url-name': 'boxing'}, 446 | {'id': 126, 447 | 'meta-tags': [{'id': 410412495320009, 448 | 'meta-tags': [{'id': 547744278460009, 449 | 'meta-tags': [], 450 | 'name': 'August 5th 2017', 451 | 'type': 'DATE', 452 | 'url-name': 'august-5th-2017'}], 453 | 'name': 'UFC', 454 | 'type': 'COMPETITION', 455 | 'url-name': 'ufc'}], 456 | 'name': 'MMA', 457 | 'type': 'SPORT', 458 | 'url-name': 'mma'}, 459 | {'id': 410468520880009, 460 | 'meta-tags': [], 461 | 'name': 'Live Betting', 462 | 'type': 'OTHER', 463 | 'url-name': 'live-betting'}, 464 | {'id': 15, 465 | 'meta-tags': [{'id': 400746807120010, 466 | 'meta-tags': [{'id': 411215851590009, 467 | 'meta-tags': [{'id': 489003593880009, 468 | 'meta-tags': [], 469 | 'name': 'Outright Winner', 470 | 'type': 'OTHER', 471 | 'url-name': 'outright-winner'}, 472 | {'id': 540818835460009, 473 | 'meta-tags': [], 474 | 'name': 'August 11th 2017', 475 | 'type': 'DATE', 476 | 'url-name': 'august-11th-2017'}, 477 | {'id': 541903114600032, 478 | 'meta-tags': [], 479 | 'name': 'August 12th 2017', 480 | 'type': 'DATE', 481 | 'url-name': 'august-12th-2017'}, 482 | {'id': 542770377450009, 483 | 'meta-tags': [], 484 | 'name': 'August 13th 2017', 485 | 'type': 'DATE', 486 | 'url-name': 'august-13th-2017'}], 487 | 'name': 'Premier League', 488 | 'type': 'COMPETITION', 489 | 'url-name': 'premier-league'}, 490 | {'id': 408548128130010, 491 | 'meta-tags': [{'id': 547114547470032, 492 | 'meta-tags': [], 493 | 'name': 'August 4th 2017', 494 | 'type': 'DATE', 495 | 'url-name': 'august-4th-2017'}, 496 | {'id': 547744278460009, 497 | 'meta-tags': [], 498 | 'name': 'August 5th 2017', 499 | 'type': 'DATE', 500 | 'url-name': 'august-5th-2017'}, 501 | {'id': 548850504600032, 502 | 'meta-tags': [], 503 | 'name': 'August 6th 2017', 504 | 'type': 'DATE', 505 | 'url-name': 'august-6th-2017'}], 506 | 'name': 'Championship', 507 | 'type': 'COMPETITION', 508 | 'url-name': 'championship'}, 509 | {'id': 555515714990032, 510 | 'meta-tags': [{'id': 548850504600032, 511 | 'meta-tags': [], 512 | 'name': 'August 6th 2017', 513 | 'type': 'DATE', 514 | 'url-name': 'august-6th-2017'}], 515 | 'name': 'FA Community Shield', 516 | 'type': 'COMPETITION', 517 | 'url-name': 'fa-community-shield'}], 518 | 'name': 'England', 519 | 'type': 'COUNTRY', 520 | 'url-name': 'england'}, 521 | {'id': 410443460160009, 522 | 'meta-tags': [{'id': 410367607550009, 523 | 'meta-tags': [{'id': 551078306630032, 524 | 'meta-tags': [], 525 | 'name': 'August 18th 2017', 526 | 'type': 'DATE', 527 | 'url-name': 'august-18th-2017'}, 528 | {'id': 547780661430032, 529 | 'meta-tags': [], 530 | 'name': 'August 19th 2017', 531 | 'type': 'DATE', 532 | 'url-name': 'august-19th-2017'}, 533 | {'id': 551078339140009, 534 | 'meta-tags': [], 535 | 'name': 'August 20th 2017', 536 | 'type': 'DATE', 537 | 'url-name': 'august-20th-2017'}], 538 | 'name': 'Bundesliga', 539 | 'type': 'COMPETITION', 540 | 'url-name': 'bundesliga'}, 541 | {'id': 555647955560032, 542 | 'meta-tags': [{'id': 547744278460009, 543 | 'meta-tags': [], 544 | 'name': 'August 5th 2017', 545 | 'type': 'DATE', 546 | 'url-name': 'august-5th-2017'}], 547 | 'name': 'German Supercup', 548 | 'type': 'COMPETITION', 549 | 'url-name': 'german-supercup'}], 550 | 'name': 'Germany', 551 | 'type': 'COUNTRY', 552 | 'url-name': 'germany'}, 553 | {'id': 292957063910136, 554 | 'meta-tags': [{'id': 405334234940009, 555 | 'meta-tags': [{'id': 547114547470032, 556 | 'meta-tags': [], 557 | 'name': 'August 4th 2017', 558 | 'type': 'DATE', 559 | 'url-name': 'august-4th-2017'}, 560 | {'id': 547744278460009, 561 | 'meta-tags': [], 562 | 'name': 'August 5th 2017', 563 | 'type': 'DATE', 564 | 'url-name': 'august-5th-2017'}, 565 | {'id': 548850504600032, 566 | 'meta-tags': [], 567 | 'name': 'August 6th 2017', 568 | 'type': 'DATE', 569 | 'url-name': 'august-6th-2017'}], 570 | 'name': 'Ligue 1 Orange', 571 | 'type': 'COMPETITION', 572 | 'url-name': 'ligue-1-orange'}], 573 | 'name': 'France', 574 | 'type': 'COUNTRY', 575 | 'url-name': 'france'}, 576 | {'id': 411234364750009, 577 | 'meta-tags': [{'id': 417188724930010, 578 | 'meta-tags': [{'id': 547114547470032, 579 | 'meta-tags': [], 580 | 'name': 'August 4th 2017', 581 | 'type': 'DATE', 582 | 'url-name': 'august-4th-2017'}, 583 | {'id': 547744278460009, 584 | 'meta-tags': [], 585 | 'name': 'August 5th 2017', 586 | 'type': 'DATE', 587 | 'url-name': 'august-5th-2017'}, 588 | {'id': 548850504600032, 589 | 'meta-tags': [], 590 | 'name': 'August 6th 2017', 591 | 'type': 'DATE', 592 | 'url-name': 'august-6th-2017'}, 593 | {'id': 555374548810032, 594 | 'meta-tags': [], 595 | 'name': 'August 7th 2017', 596 | 'type': 'DATE', 597 | 'url-name': 'august-7th-2017'}], 598 | 'name': 'ALKA Superliga', 599 | 'type': 'COMPETITION', 600 | 'url-name': 'alka-superliga'}], 601 | 'name': 'Denmark', 602 | 'type': 'COUNTRY', 603 | 'url-name': 'denmark'}, 604 | {'id': 405440699510010, 605 | 'meta-tags': [{'id': 410261886050010, 606 | 'meta-tags': [{'id': 551078339140009, 607 | 'meta-tags': [], 608 | 'name': 'August 20th 2017', 609 | 'type': 'DATE', 610 | 'url-name': 'august-20th-2017'}], 611 | 'name': 'La Liga', 612 | 'type': 'COMPETITION', 613 | 'url-name': 'la-liga'}], 614 | 'name': 'Spain', 615 | 'type': 'COUNTRY', 616 | 'url-name': 'spain'}, 617 | {'id': 414996609190010, 618 | 'meta-tags': [{'id': 483687521130009, 619 | 'meta-tags': [{'id': 548851381700032, 620 | 'meta-tags': [], 621 | 'name': 'July 31st 2017', 622 | 'type': 'DATE', 623 | 'url-name': 'july-31st-2017'}], 624 | 'name': 'Série A', 625 | 'type': 'COMPETITION', 626 | 'url-name': 'série-a'}], 627 | 'name': 'Brazil', 628 | 'type': 'COUNTRY', 629 | 'url-name': 'brazil'}, 630 | {'id': 407888102180009, 631 | 'meta-tags': [{'id': 551190350810032, 632 | 'meta-tags': [], 633 | 'name': 'August 1st 2017', 634 | 'type': 'DATE', 635 | 'url-name': 'august-1st-2017'}, 636 | {'id': 551626703180032, 637 | 'meta-tags': [], 638 | 'name': 'August 2nd 2017', 639 | 'type': 'DATE', 640 | 'url-name': 'august-2nd-2017'}], 641 | 'name': 'UEFA Champions League', 642 | 'type': 'COMPETITION', 643 | 'url-name': 'uefa-champions-league'}, 644 | {'id': 410316590050010, 645 | 'meta-tags': [{'id': 551626703180032, 646 | 'meta-tags': [], 647 | 'name': 'August 2nd 2017', 648 | 'type': 'DATE', 649 | 'url-name': 'august-2nd-2017'}, 650 | {'id': 551626692040009, 651 | 'meta-tags': [], 652 | 'name': 'August 3rd 2017', 653 | 'type': 'DATE', 654 | 'url-name': 'august-3rd-2017'}], 655 | 'name': 'UEFA Europa League', 656 | 'type': 'COMPETITION', 657 | 'url-name': 'uefa-europa-league'}, 658 | {'id': 410444698700010, 659 | 'meta-tags': [{'id': 414535973650010, 660 | 'meta-tags': [{'id': 547780661430032, 661 | 'meta-tags': [], 662 | 'name': 'August 19th 2017', 663 | 'type': 'DATE', 664 | 'url-name': 'august-19th-2017'}], 665 | 'name': 'Serie A TIM', 666 | 'type': 'COMPETITION', 667 | 'url-name': 'serie-a-tim'}], 668 | 'name': 'Italy', 669 | 'type': 'COUNTRY', 670 | 'url-name': 'italy'}, 671 | {'id': 446588418200010, 672 | 'meta-tags': [{'id': 469325478350010, 673 | 'meta-tags': [{'id': 548850504600032, 674 | 'meta-tags': [], 675 | 'name': 'August 6th 2017', 676 | 'type': 'DATE', 677 | 'url-name': 'august-6th-2017'}, 678 | {'id': 547744278460009, 679 | 'meta-tags': [], 680 | 'name': 'August 5th 2017', 681 | 'type': 'DATE', 682 | 'url-name': 'august-5th-2017'}], 683 | 'name': 'Eliteserien', 684 | 'type': 'COMPETITION', 685 | 'url-name': 'eliteserien'}], 686 | 'name': 'Norway', 687 | 'type': 'COUNTRY', 688 | 'url-name': 'norway'}, 689 | {'id': 408557473770009, 690 | 'meta-tags': [{'id': 555649150350032, 691 | 'meta-tags': [{'id': 547744278460009, 692 | 'meta-tags': [], 693 | 'name': 'August 5th 2017', 694 | 'type': 'DATE', 695 | 'url-name': 'august-5th-2017'}], 696 | 'name': 'Johan Cruijff Shield', 697 | 'type': 'COMPETITION', 698 | 'url-name': 'johan-cruijff-shield'}], 699 | 'name': 'Netherlands', 700 | 'type': 'COUNTRY', 701 | 'url-name': 'netherlands'}, 702 | {'id': 411343363720010, 703 | 'meta-tags': [{'id': 555665504260032, 704 | 'meta-tags': [{'id': 547744278460009, 705 | 'meta-tags': [], 706 | 'name': 'August 5th 2017', 707 | 'type': 'DATE', 708 | 'url-name': 'august-5th-2017'}], 709 | 'name': 'Portuguese Supercup', 710 | 'type': 'COMPETITION', 711 | 'url-name': 'portuguese-supercup'}], 712 | 'name': 'Portugal', 713 | 'type': 'COUNTRY', 714 | 'url-name': 'portugal'}, 715 | {'id': 412057448250009, 716 | 'meta-tags': [{'id': 417251335140010, 717 | 'meta-tags': [{'id': 547744278460009, 718 | 'meta-tags': [], 719 | 'name': 'August 5th 2017', 720 | 'type': 'DATE', 721 | 'url-name': 'august-5th-2017'}, 722 | {'id': 548850504600032, 723 | 'meta-tags': [], 724 | 'name': 'August 6th 2017', 725 | 'type': 'DATE', 726 | 'url-name': 'august-6th-2017'}], 727 | 'name': 'Raiffeisen Super League', 728 | 'type': 'COMPETITION', 729 | 'url-name': 'raiffeisen-super-league'}], 730 | 'name': 'Switzerland', 731 | 'type': 'COUNTRY', 732 | 'url-name': 'switzerland'}], 733 | 'name': 'Soccer', 734 | 'type': 'SPORT', 735 | 'url-name': 'soccer'}], 736 | 'name': 'Sport', 737 | 'tree-id': 1, 738 | 'url-name': 'sport'}, 739 | {'id': 2, 740 | 'meta-tags': [], 741 | 'name': 'Country', 742 | 'tree-id': 2, 743 | 'url-name': 'country'}, 744 | {'id': 3, 'meta-tags': [], 'name': 'Date', 'tree-id': 3, 'url-name': 'date'}] --------------------------------------------------------------------------------