From cc01b1a33a61e1f93f0b32509f3bc8ebe62a9954 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Sun, 17 Jan 2021 16:50:05 +0100 Subject: [PATCH 01/14] move kambi parser in kambi.py --- soccerapi/api/base.py | 139 +++-------------------------------------- soccerapi/api/kambi.py | 82 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 130 deletions(-) create mode 100644 soccerapi/api/kambi.py diff --git a/soccerapi/api/base.py b/soccerapi/api/base.py index 0402f75..255c6a6 100644 --- a/soccerapi/api/base.py +++ b/soccerapi/api/base.py @@ -1,15 +1,13 @@ import abc -from typing import Dict, List, Tuple - -import requests +from typing import List, Tuple class ApiBase(abc.ABC): """ The Abstract Base Class on which every Api[Boolmaker] is based on. """ @abc.abstractmethod - def _requests(self, competition: str, **kwargs) -> Tuple: - """ Perform requests to site and get data_to_parse """ + def requests(self, competition: str, **kwargs) -> Tuple: + """ Open a requests.Session and perform _request """ pass @abc.abstractmethod @@ -20,135 +18,16 @@ def competition(self, url: str) -> str: pass def odds(self, url: str) -> List: - """ Get odds from country-league competition or from url """ - - odds = [] - competition = self.competition(url) - data_to_parse = self._requests(competition) - - for data, parser in zip(data_to_parse, self.parsers): - try: - odds.append(parser(data)) - except NoOddsError: - # sometimes some odds categories aren't avaiable - pass - - for category in odds[1:]: - for i, event in enumerate(category): - odds[0][i] = {**odds[0][i], **event} - - # If no odds are found the result from: - # - Kambi based api is [[], [], []] - # - bet365 is [] - if len(sum(odds, [])) > 0: - return odds[0] - else: - msg = f'No odds in {url} have been found.' - raise NoOddsError(msg) + """ Get odds from url """ + odds = self.requests(self.competition(url)) + # parse odds + for parser, data in odds.items(): + odds[parser] = getattr(self, parser)(data) -class ApiKambi(ApiBase): - """888sport, unibet and other use the same CDN (eu-offering.kambicdn) - so the requetsting and parsing process is exaclty the same. - The only thing that chage is the base_url and the competition method""" - - @staticmethod - def _full_time_result(data: Dict) -> List: - """ Parse the raw json requests for full_time_result """ - - odds = [] - for event in data['events']: - if event['event']['state'] == 'STARTED': - continue - try: - full_time_result = { - '1': event['betOffers'][0]['outcomes'][0].get('odds'), - 'X': event['betOffers'][0]['outcomes'][1].get('odds'), - '2': event['betOffers'][0]['outcomes'][2].get('odds'), - } - except IndexError: - full_time_result = None - - odds.append( - { - 'time': event['event']['start'], - 'home_team': event['event']['homeName'], - 'away_team': event['event']['awayName'], - 'full_time_resut': full_time_result, - } - ) + # TODO merge odds return odds - @staticmethod - def _both_teams_to_score(data: Dict) -> List: - """ Parse the raw json requests for both_teams_to_score """ - - odds = [] - for event in data['events']: - if event['event']['state'] == 'STARTED': - continue - try: - both_teams_to_score = { - 'yes': event['betOffers'][0]['outcomes'][0].get('odds'), - 'no': event['betOffers'][0]['outcomes'][1].get('odds'), - } - except IndexError: - both_teams_to_score = None - odds.append( - { - 'time': event['event']['start'], - 'home_team': event['event']['homeName'], - 'away_team': event['event']['awayName'], - 'both_teams_to_score': both_teams_to_score, - } - ) - return odds - - @staticmethod - def _double_chance(data: Dict) -> List: - """ Parse the raw json requests for double chance """ - - odds = [] - for event in data['events']: - if event['event']['state'] == 'STARTED': - continue - try: - double_chance = { - '1X': event['betOffers'][0]['outcomes'][0].get('odds'), - '12': event['betOffers'][0]['outcomes'][1].get('odds'), - '2X': event['betOffers'][0]['outcomes'][2].get('odds'), - } - except IndexError: - double_chance = None - odds.append( - { - 'time': event['event']['start'], - 'home_team': event['event']['homeName'], - 'away_team': event['event']['awayName'], - 'double_chance': double_chance, - } - ) - return odds - - def _requests(self, competition: str, market: str = 'IT') -> Tuple[Dict]: - """Build URL starting from country and league and request data for - - full_time_result - - both_teams_to_score - - double_chance - """ - s = requests.Session() - base_params = {'lang': 'en_US', 'market': market} - url = '/'.join([self.base_url, competition]) + '.json' - - return ( - # full_time_result 12579 - s.get(url, params={**base_params, 'category': 12579}).json(), - # both_teams_to_score 11942 - s.get(url, params={**base_params, 'category': 11942}).json(), - # double_chance 12220 - s.get(url, params={**base_params, 'category': 12220}).json(), - ) - class NoOddsError(Exception): """ No odds are found. """ diff --git a/soccerapi/api/kambi.py b/soccerapi/api/kambi.py new file mode 100644 index 0000000..a351d30 --- /dev/null +++ b/soccerapi/api/kambi.py @@ -0,0 +1,82 @@ +from typing import Dict, List + + +class ParserKambi: + """888sport, unibet and other use the same CDN (eu-offering.kambicdn) + so the requetsting and parsing process is exaclty the same. + This class implements parsers for variuos category for Kambi.""" + + def full_time_result(self, data: Dict) -> List: + """ Parse the raw json requests for full_time_result """ + + odds = [] + for event in data['events']: + if event['event']['state'] == 'STARTED': + continue + try: + full_time_result = { + '1': event['betOffers'][0]['outcomes'][0].get('odds'), + 'X': event['betOffers'][0]['outcomes'][1].get('odds'), + '2': event['betOffers'][0]['outcomes'][2].get('odds'), + } + except IndexError: + full_time_result = None + + odds.append( + { + 'time': event['event']['start'], + 'home_team': event['event']['homeName'], + 'away_team': event['event']['awayName'], + 'full_time_resut': full_time_result, + } + ) + return odds + + def both_teams_to_score(self, data: Dict) -> List: + """ Parse the raw json requests for both_teams_to_score """ + + odds = [] + for event in data['events']: + if event['event']['state'] == 'STARTED': + continue + try: + both_teams_to_score = { + 'yes': event['betOffers'][0]['outcomes'][0].get('odds'), + 'no': event['betOffers'][0]['outcomes'][1].get('odds'), + } + except IndexError: + both_teams_to_score = None + odds.append( + { + 'time': event['event']['start'], + 'home_team': event['event']['homeName'], + 'away_team': event['event']['awayName'], + 'both_teams_to_score': both_teams_to_score, + } + ) + return odds + + def double_chance(self, data: Dict) -> List: + """ Parse the raw json requests for double chance """ + + odds = [] + for event in data['events']: + if event['event']['state'] == 'STARTED': + continue + try: + double_chance = { + '1X': event['betOffers'][0]['outcomes'][0].get('odds'), + '12': event['betOffers'][0]['outcomes'][1].get('odds'), + '2X': event['betOffers'][0]['outcomes'][2].get('odds'), + } + except IndexError: + double_chance = None + odds.append( + { + 'time': event['event']['start'], + 'home_team': event['event']['homeName'], + 'away_team': event['event']['awayName'], + 'double_chance': double_chance, + } + ) + return odds From d733af64ffd27195e8f064f06e9469128b3857fe Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Sun, 17 Jan 2021 16:50:50 +0100 Subject: [PATCH 02/14] refactoring 888sport.py with new structure --- soccerapi/api/888sport.py | 44 +++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/soccerapi/api/888sport.py b/soccerapi/api/888sport.py index b03155c..79471f9 100644 --- a/soccerapi/api/888sport.py +++ b/soccerapi/api/888sport.py @@ -1,22 +1,18 @@ import re +from typing import Dict, Tuple -from .base import ApiKambi +import requests +from .base import ApiBase +from .kambi import ParserKambi as Parser888Sport -class Api888Sport(ApiKambi): + +class Api888Sport(ApiBase, Parser888Sport): """ The ApiKambi implementation for 888sport.com """ def __init__(self): self.name = '888sport' - self.base_url = ( - 'https://eu-offering.kambicdn.org/' - 'offering/v2018/888/listView/football' - ) - self.parsers = [ - self._full_time_result, - self._both_teams_to_score, - self._double_chance, - ] + self.session = requests.Session() def competition(self, url: str) -> str: re_888sport = re.compile( @@ -28,3 +24,29 @@ def competition(self, url: str) -> str: else: msg = f'Cannot parse {url}' raise ValueError(msg) + + def requests(self, competition: str) -> Tuple[Dict]: + return { + 'full_time_result': self._request(competition, 12579), + 'both_teams_to_score': self._request(competition, 11942), + 'double_chance': self._request(competition, 12220), + } + + # Auxiliary methods + + def _request( + self, competition: str, category: int, market: str = 'IT' + ) -> Dict: + """ Make the single request using the active session """ + + base_url = ( + 'https://eu-offering.kambicdn.org/' + 'offering/v2018/888/listView/football' + ) + url = '/'.join([base_url, competition]) + '.json' + params = ( + ('lang', 'en_US'), + ('market', market), + ('category', category), + ) + return self.session.get(url, params=params).json() From 24412ea94fd6ef4ce7bf82522c1c428cff955dc0 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Sun, 17 Jan 2021 16:51:14 +0100 Subject: [PATCH 03/14] refactoring bet365.py with new structure and fix time bug --- soccerapi/api/bet365.py | 228 ++++++++++++++++++---------------------- 1 file changed, 104 insertions(+), 124 deletions(-) diff --git a/soccerapi/api/bet365.py b/soccerapi/api/bet365.py index af052c7..2d905b8 100644 --- a/soccerapi/api/bet365.py +++ b/soccerapi/api/bet365.py @@ -7,16 +7,74 @@ from .base import ApiBase, NoOddsError -class ApiBet365(ApiBase): - """ The ApiBase implementation of bet365.com """ +class ParserBet365: + """ Implementation of parsers for ApiBet365 """ - def __init__(self): - self.name = 'bet365' - self.parsers = [ - self._full_time_result, - self._both_teams_to_score, - self._double_chance, - ] + def full_time_result(self, data: str) -> List: + odds = [] + events = self._parse_events(data) + full_time_result = self._parse_odds(data) + + assert len(events) == len(full_time_result) / 3 + + # old format, store by rows + # _1s = full_time_result[0::3] + # _Xs = full_time_result[1::3] + # _2s = full_time_result[2::3] + + # new foramt + le = len(events) + assert le == len(full_time_result) / 3 + _1s = full_time_result[:le] + _Xs = full_time_result[le : 2 * le] + _2s = full_time_result[2 * le :] + + for event, _1, _X, _2 in zip(events, _1s, _Xs, _2s): + odds.append({**event, 'odds': {'1': _1, 'X': _X, '2': _2}}) + return odds + + def both_teams_to_score(self, data: str) -> List: + odds = [] + events = self._parse_events(data) + both_teams_to_score = self._parse_odds(data) + + # old format, store by rows + # yess = both_teams_to_score[0::2] + # nos = both_teams_to_score[1::2] + + # new foramt + assert len(events) == len(both_teams_to_score) / 2 + yess = both_teams_to_score[: len(events)] + nos = both_teams_to_score[len(events) :] + + for event, yes, no in zip(events, yess, nos): + odds.append({**event, 'odds': {'yes': yes, 'no': no}}) + + return odds + + def double_chance(self, data: str) -> List: + odds = [] + events = self._parse_events(data) + double_chance = self._parse_odds(data) + + # old format, store by rows + # _1Xs = double_chance[0::3] + # _2Xs = double_chance[1::3] + # _12s = double_chance[2::3] + + # new foramt + le = len(events) + assert le == len(double_chance) / 3 + _1Xs = double_chance[:le] + _2Xs = double_chance[le : 2 * le] + _12s = double_chance[2 * le :] + + for event, _1X, _2X, _12 in zip(events, _1Xs, _2Xs, _12s): + odds.append({**event, 'odds': {'1X': _1X, '12': _12, '2X': _2X}}) + + return odds + + # Auxiliary methods @staticmethod def _xor(msg: str, key: int) -> str: @@ -102,111 +160,33 @@ def _parse_events(self, data: str) -> List: home_teams, away_teams = self._parse_teams(data) for dt, home_team, away_team in zip(datetimes, home_teams, away_teams): - if dt > datetime.now(): + if dt > datetime.utcnow(): events.append( {'time': dt, 'home_team': home_team, 'away_team': away_team} ) return events - def _full_time_result(self, data: str) -> List: - """ Parse the raw data for full_time_result [13]""" - - odds = [] - events = self._parse_events(data) - full_time_result = self._parse_odds(data) - - assert len(events) == len(full_time_result) / 3 - - # old format, store by rows - # _1s = full_time_result[0::3] - # _Xs = full_time_result[1::3] - # _2s = full_time_result[2::3] - - # new foramt - le = len(events) - assert le == len(full_time_result) / 3 - _1s = full_time_result[:le] - _Xs = full_time_result[le : 2 * le] - _2s = full_time_result[2 * le :] - - for event, _1, _X, _2 in zip(events, _1s, _Xs, _2s): - odds.append( - {**event, 'full_time_result': {'1': _1, 'X': _X, '2': _2}} - ) - return odds - def _both_teams_to_score(self, data: str) -> List: - """ Parse the raw data for both_teams_to_score [170]""" - - odds = [] - events = self._parse_events(data) - both_teams_to_score = self._parse_odds(data) - - # old format, store by rows - # yess = both_teams_to_score[0::2] - # nos = both_teams_to_score[1::2] - - # new foramt - assert len(events) == len(both_teams_to_score) / 2 - yess = both_teams_to_score[: len(events)] - nos = both_teams_to_score[len(events) :] - - for event, yes, no in zip(events, yess, nos): - odds.append( - {**event, 'both_teams_to_score': {'yes': yes, 'no': no}} - ) - - return odds - - def _double_chance(self, data: str) -> List: - """ Parse the raw data for double_chance [195]""" - - odds = [] - events = self._parse_events(data) - double_chance = self._parse_odds(data) - - # old format, store by rows - # _1Xs = double_chance[0::3] - # _2Xs = double_chance[1::3] - # _12s = double_chance[2::3] - - # new foramt - le = len(events) - assert le == len(double_chance) / 3 - _1Xs = double_chance[:le] - _2Xs = double_chance[le : 2 * le] - _12s = double_chance[2 * le :] - - for event, _1X, _2X, _12 in zip(events, _1Xs, _2Xs, _12s): - odds.append( - {**event, 'double_chance': {'1X': _1X, '12': _12, '2X': _2X}} - ) - - return odds +class ApiBet365(ApiBase, ParserBet365): + """ The ApiBase implementation of bet365.com """ - def _request( - self, s: requests.Session, competition: str, category: int - ) -> str: - """ Make the single request using the active session """ + def __init__(self): + self.name = 'bet365' + self.session = requests.Session() - url = 'https://www.bet365.it/SportsBook.API/web' - params = ( - ('lid', '1'), - ('zid', '0'), - ('pd', f'#AC#B1#C1#D{category}#{competition}#F2#'), - ('cid', '97'), - ('ctid', '97'), + def competition(self, url: str) -> str: + re_bet365 = re.compile( + r'https?://www\.bet365\.\w{2,3}/#/' + r'[0-9a-fA-F/]*/D[0-9]+/[0-9a-fA-F]{9}/[0-9a-fA-F]{2}/?' ) - return s.get(url, params=params).text - - def _requests(self, competition: str, **kwargs) -> Tuple[Dict]: - """Build URL starting from league (an unique id) and requests data for - - full_time_result - - both_teams_to_score - - double_chance - """ + if re_bet365.match(url): + return url.split('/')[8] + else: + msg = f'Cannot parse {url}' + raise ValueError(msg) + def requests(self, competition: str) -> Tuple[Dict]: config_url = 'https://www.bet365.it/defaultapi/sports-configuration' cookies = {'aps03': 'ct=97&lng=6'} headers = { @@ -225,28 +205,28 @@ def _requests(self, competition: str, **kwargs) -> Tuple[Dict]: 'Chrome/79.0.3945.117 Safari/537.36' ), } - s = requests.Session() - s.headers.update(headers) - s.get(config_url, cookies=cookies) - - return ( - # full_time_result 13 - self._request(s, competition, 13), - # both_teams_to_score 170 - self._request(s, competition, 170), - # double_chance 195 - self._request(s, competition, 195), + self.session.headers.update(headers) + self.session.get(config_url, cookies=cookies) + + return { + 'full_time_result': self._request(competition, 13), + 'both_teams_to_score': self._request(competition, 170), + 'double_chance': self._request(competition, 195), # under_over 56 # self._request(s, competition, 56), - ) + } - def competition(self, url: str) -> str: - re_bet365 = re.compile( - r'https?://www\.bet365\.\w{2,3}/#/' - r'[0-9a-fA-F/]*/D[0-9]+/[0-9a-fA-F]{9}/[0-9a-fA-F]{2}/?' + # Auxiliary methods + + def _request(self, competition: str, category: int) -> str: + """ Make the single request using the active session """ + + url = 'https://www.bet365.it/SportsBook.API/web' + params = ( + ('lid', '1'), + ('zid', '0'), + ('pd', f'#AC#B1#C1#D{category}#{competition}#F2#'), + ('cid', '97'), + ('ctid', '97'), ) - if re_bet365.match(url): - return url.split('/')[8] - else: - msg = f'Cannot parse {url}' - raise ValueError(msg) + return self.session.get(url, params=params).text From 529713cc7d51b2965201f69c417131d8fc15a325 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Sun, 17 Jan 2021 16:51:33 +0100 Subject: [PATCH 04/14] refactoring unibet.py with new structure --- soccerapi/api/unibet.py | 48 ++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/soccerapi/api/unibet.py b/soccerapi/api/unibet.py index 36adbcf..c5d05f6 100644 --- a/soccerapi/api/unibet.py +++ b/soccerapi/api/unibet.py @@ -1,22 +1,18 @@ import re +from typing import Dict, Tuple -from .base import ApiKambi +import requests +from .base import ApiBase +from .kambi import ParserKambi as ParserUnibet -class ApiUnibet(ApiKambi): - """ The ApiBase implementation for unibet.com """ + +class ApiUnibet(ApiBase, ParserUnibet): + """ The ApiKambi implementation for unibet.com """ def __init__(self): self.name = 'unibet' - self.base_url = ( - 'https://eu-offering.kambicdn.org/' - 'offering/v2018/ub/listView/football' - ) - self.parsers = [ - self._full_time_result, - self._both_teams_to_score, - self._double_chance, - ] + self.session = requests.Session() def competition(self, url: str) -> str: re_unibet = re.compile( @@ -28,3 +24,31 @@ def competition(self, url: str) -> str: else: msg = f'Cannot parse {url}' raise ValueError(msg) + + def requests(self, competition: str) -> Tuple[Dict]: + return { + 'full_time_result': self._request(competition, 12579), + 'both_teams_to_score': self._request(competition, 11942), + 'double_chance': self._request(competition, 12220), + } + + # Parsers (implemented in ApiKambi) + + # Auxiliary methods + + def _request( + self, competition: str, category: int, market: str = 'IT' + ) -> Dict: + """ Make the single request using the active session """ + + base_url = ( + 'https://eu-offering.kambicdn.org/' + 'offering/v2018/ub/listView/football' + ) + url = '/'.join([base_url, competition]) + '.json' + params = ( + ('lang', 'en_US'), + ('market', market), + ('category', category), + ) + return self.session.get(url, params=params).json() From 3dd838b86f0f92b68b2efa7774412a7d6654122d Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Sun, 17 Jan 2021 16:52:08 +0100 Subject: [PATCH 05/14] create example.py, help contributions --- soccerapi/api/example.py | 116 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 soccerapi/api/example.py diff --git a/soccerapi/api/example.py b/soccerapi/api/example.py new file mode 100644 index 0000000..c3917af --- /dev/null +++ b/soccerapi/api/example.py @@ -0,0 +1,116 @@ +from typing import Dict, List + +import requests + +from .base import ApiBase + + +class ParserExample: + """ Implementation of parsers for ApiExample """ + + # Parser class implements the data parsers (i.e. methods that take + # raw data, json data, obfuscated data return them in an organized form) + + # Parser retrun list of events + odds. + # Example for full_time_result -> [ + # { + # 'time': datetime.datetime(2021, 1, 17, 12, 17, 39), + # 'home_team': 'juventus', + # 'away_team': 'inter', + # 'odds': {'1': 1500, 'X': 1500, '2': 3350} + # }, + # ... + # { + # 'time': datetime.datetime(2021, 1, 20, 12, 17, 39), + # 'home_team': 'milan', + # 'away_team': 'torino', + # 'odds': {'1': 1250, 'X': 1500, '2': 4500} + # }, + # ] + # + # 'time': python datetime object when the match starts + # 'home_team': name of the home team that appears on the bookmaker site + # 'away_team': name of the away team that appears on the bookmaker site + # 'odds': dict of of odds in decimal (millesimal) format; the keys of the + # dict depend on the parser type. + + # Type of parsers: + # - full_time_result -> { '1': 1250, 'X': 1500, '2': 4500} + # - double_chance -> { '1X': 1250, '12': 1500, '2X': 4500} + # - both_teams_to_score -> {'yes': 1250, 'no': 1500} + + def full_time_result(self, data) -> List[Dict]: + + # events = list of events parsed from data + # odds = list of odds in decimal format parsed from data + + # For extraxting events or odds from row data, sometimes it's usefull + # to define some auxiliary methods (e.g. parsing the time, teams or + # for deobfuscate odds). Name of those methods should starts with + # underscore (e.g. _time, _teams, ...) + + ... + + # return [ + # {**e, 'odds': {'1': ..., 'X': ..., '2': ...}} + # for i, (e, o) in enumerate(zip(events, odds)) + # ] + + def both_teams_to_score(self, data) -> List[Dict]: + ... + + def double_chance(self, data) -> List[Dict]: + ... + + # Auxiliary methods + + # def _time(self, data): + # ... + # + # def _teams(self, data): + # ... + # + # def _events(self, data): + # ... + # + # def _odds(self, data): + # ... + + +class ApiExample(ApiBase, ParserExample): + """ Api for Example bookmaker """ + + def __init__(self): + # bookmaker name + self.name = 'example' + + # requests session used to the request to bookmaker site + self.session = requests.Session() + + def competition(self, url: str) -> str: + # Parese the bookmaker url containing the competition information + # using some kind of regex + ... + # return 'Italia-SerieA' + + def requests(self, competition: str): + # Perform requeests to various endpoints in order to get odds + # for different categories (e.g. full_time_result, double_chance, ...) + + # Define here the session.headers and cookies. + + # Sometime is useful define an auxiliary method _request which + # accepts the category param and send correct headers and params. + # Auxiliary methods names should start with underscore (e.g. _request). + + ... + # return { + # 'full_time_result': self._request(competition, 13), + # 'both_teams_to_score': self._request(competition, 170), + # 'double_chance': self._request(competition, 195), + # } + + # Auxiliary methods + + # def _request(self, competition: str, category: int): + # ... From 26a5fa015f31d779439899855444ca5805ca0804 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Sun, 17 Jan 2021 17:04:57 +0100 Subject: [PATCH 06/14] write your API section contributing.md --- CONTRIBUTING.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35c669c..aa1c6e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,9 +15,11 @@ so I can update the documentation. have to install it on your local machine. Then clone this Github repository with + ```bash git clone https://github.com/S1M0N38/soccerapi.git ``` + and then go into it ```bash @@ -25,11 +27,29 @@ cd soccerapi ``` Then install the dependencies with + ```bash poetry install ``` +Finally activate the virtual env with + +```bash +poetry shell +``` + ## Running the test suite Go into soccerapi directory (the same directory where is README.md) and run -tests with `pipenv run pytest`. +tests with `p`. + +## Writing Api for new bookmaker + +Another way to contribute is to write a Api class for a new bookmaker. +For writing such class take a look at +[example.py](https://github.com/S1M0N38/soccerapi/blob/master/soccerapi/api/example.py) +or at the already implemented Api classes ( +[888sport.py](https://github.com/S1M0N38/soccerapi/blob/master/soccerapi/api/888sport.py), +[bet365.py](https://github.com/S1M0N38/soccerapi/blob/master/soccerapi/api/bet365.py), +[unibet.py](https://github.com/S1M0N38/soccerapi/blob/master/soccerapi/api/unibet.py), +). From 524dedfa7824750836ad1cd6618931714645d9b1 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Sun, 17 Jan 2021 17:06:42 +0100 Subject: [PATCH 07/14] add example.py in __init__.py --- soccerapi/api/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/soccerapi/api/__init__.py b/soccerapi/api/__init__.py index c02bc20..c03b2ae 100644 --- a/soccerapi/api/__init__.py +++ b/soccerapi/api/__init__.py @@ -5,3 +5,5 @@ Api888Sport = importlib.import_module('.888sport', package).Api888Sport ApiBet365 = importlib.import_module('.bet365', package).ApiBet365 ApiUnibet = importlib.import_module('.unibet', package).ApiUnibet + +# ApiExample = importlib.import_module('.example', package).ApiExample From 1bb4f0e38705a7ad85f25bbb8f8f96d75c4702a6 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Sun, 17 Jan 2021 17:08:57 +0100 Subject: [PATCH 08/14] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa1c6e1..225a19b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ poetry shell ## Running the test suite Go into soccerapi directory (the same directory where is README.md) and run -tests with `p`. +tests with `pytest`. ## Writing Api for new bookmaker From 800e97b4b341e4386e0713867dc45c15024c6526 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Tue, 19 Jan 2021 19:26:14 +0100 Subject: [PATCH 09/14] no more obfuscation key guessing --- soccerapi/api/bet365.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/soccerapi/api/bet365.py b/soccerapi/api/bet365.py index 2d905b8..7615272 100644 --- a/soccerapi/api/bet365.py +++ b/soccerapi/api/bet365.py @@ -138,7 +138,10 @@ def _parse_odds(self, data: str) -> List: values = self._get_values(data, 'OD') if len(values) == 0: raise NoOddsError - key = self._guess_xor_key(values[0]) + + TK = data.split(';')[1][3:] + key = ord(TK[0]) ^ ord(TK[1]) + # key = self._guess_xor_key(values[0]) for obfuscated_odd in values: # Event exists but no odds are available From b51bc92bd058555335679d918d824886f945afb4 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Wed, 20 Jan 2021 19:08:47 +0100 Subject: [PATCH 10/14] move pytest to dev-deps --- poetry.lock | 40 ++++++++++++++++++++-------------------- pyproject.toml | 4 ++-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1585efd..5f648aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,7 +2,7 @@ name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -10,7 +10,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "20.3.0" description = "Classes Without Boilerplate" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -40,7 +40,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -48,7 +48,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "coverage" version = "5.3.1" description = "Code coverage measurement for Python" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" @@ -67,7 +67,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "importlib-metadata" version = "3.4.0" description = "Read metadata from Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -83,7 +83,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -91,7 +91,7 @@ python-versions = "*" name = "packaging" version = "20.8" description = "Core utilities for Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -102,7 +102,7 @@ pyparsing = ">=2.0.2" name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -116,7 +116,7 @@ dev = ["pre-commit", "tox"] name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -124,7 +124,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "pyparsing" version = "2.4.7" description = "Python parsing module" -category = "main" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -132,7 +132,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "pytest" version = "6.2.1" description = "pytest: simple powerful testing with Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -152,14 +152,14 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-cov" -version = "2.10.1" +version = "2.11.1" description = "Pytest plugin for measuring coverage." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -coverage = ">=4.4" +coverage = ">=5.2.1" pytest = ">=4.6" [package.extras] @@ -187,7 +187,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -195,7 +195,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "typing-extensions" version = "3.7.4.3" description = "Backported and Experimental Type Hints for Python 3.5+" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -216,7 +216,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "zipp" version = "3.4.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -227,7 +227,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "057df11d8ff65e262ecf951b931f24654d2ef55e74516db609548e76484dc0d0" +content-hash = "b73b48e95da321e1d365868a12f45d6e0fb9273d67f8c008437e1e6f73ce38fc" [metadata.files] atomicwrites = [ @@ -334,8 +334,8 @@ pytest = [ {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, ] pytest-cov = [ - {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, - {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, + {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, + {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, ] requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, diff --git a/pyproject.toml b/pyproject.toml index 7558c71..a43c691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,10 +10,10 @@ homepage = "https://github.com/S1M0N38/soccerapi" [tool.poetry.dependencies] python = "^3.7" requests = "^2.25.1" -pytest = "^6.2.1" -pytest-cov = "^2.10.1" [tool.poetry.dev-dependencies] +pytest = "^6.2.1" +pytest-cov = "^2.10.1" [build-system] requires = ["poetry-core>=1.0.0"] From 33c4119e593d32b1e14f4ed110da39dad402218a Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Wed, 20 Jan 2021 19:11:18 +0100 Subject: [PATCH 11/14] new parser models kambi: 'odds' insted of '' --- soccerapi/api/kambi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/soccerapi/api/kambi.py b/soccerapi/api/kambi.py index a351d30..ec31acb 100644 --- a/soccerapi/api/kambi.py +++ b/soccerapi/api/kambi.py @@ -27,7 +27,7 @@ def full_time_result(self, data: Dict) -> List: 'time': event['event']['start'], 'home_team': event['event']['homeName'], 'away_team': event['event']['awayName'], - 'full_time_resut': full_time_result, + 'odds': full_time_result, } ) return odds @@ -51,7 +51,7 @@ def both_teams_to_score(self, data: Dict) -> List: 'time': event['event']['start'], 'home_team': event['event']['homeName'], 'away_team': event['event']['awayName'], - 'both_teams_to_score': both_teams_to_score, + 'odds': both_teams_to_score, } ) return odds @@ -76,7 +76,7 @@ def double_chance(self, data: Dict) -> List: 'time': event['event']['start'], 'home_team': event['event']['homeName'], 'away_team': event['event']['awayName'], - 'double_chance': double_chance, + 'odds': double_chance, } ) return odds From b2774ad8286b9c13fe8a1128c2a734cdd3c9eef2 Mon Sep 17 00:00:00 2001 From: musso Date: Mon, 25 Jan 2021 00:54:55 +0100 Subject: [PATCH 12/14] format odds in a event list --- soccerapi/api/base.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/soccerapi/api/base.py b/soccerapi/api/base.py index 255c6a6..69cbdbd 100644 --- a/soccerapi/api/base.py +++ b/soccerapi/api/base.py @@ -3,7 +3,7 @@ class ApiBase(abc.ABC): - """ The Abstract Base Class on which every Api[Boolmaker] is based on. """ + """ The Abstract Base Class on which every Api[Bookmaker] is based on. """ @abc.abstractmethod def requests(self, competition: str, **kwargs) -> Tuple: @@ -19,13 +19,24 @@ def competition(self, url: str) -> str: def odds(self, url: str) -> List: """ Get odds from url """ - odds = self.requests(self.competition(url)) + to_parse = self.requests(self.competition(url)) # parse odds - for parser, data in odds.items(): - odds[parser] = getattr(self, parser)(data) + to_format = dict() + for parser, data in to_parse.items(): + to_format[parser]= getattr(self, parser)(data) # why not "self.parser(data)" ?? + + # TODO format odds + # get list of events + odds = to_format[list(to_parse.keys())[0]].copy() + for ind, event in enumerate(odds): + for parser in to_parse.keys(): + # add market (parser) to event + event[parser] = to_format[parser][ind].get('odds') + # pop inherited odds key + event.pop('odds') + odds[ind] = event.copy() - # TODO merge odds return odds From 371843d9be9a89285a8bb442d7c2d7b3745b9aa1 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Mon, 25 Jan 2021 01:35:04 +0100 Subject: [PATCH 13/14] clean up the odds merging process --- soccerapi/api/base.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/soccerapi/api/base.py b/soccerapi/api/base.py index 69cbdbd..61d8b85 100644 --- a/soccerapi/api/base.py +++ b/soccerapi/api/base.py @@ -1,4 +1,5 @@ import abc +from pprint import pprint from typing import List, Tuple @@ -19,23 +20,29 @@ def competition(self, url: str) -> str: def odds(self, url: str) -> List: """ Get odds from url """ - to_parse = self.requests(self.competition(url)) + + odds_to_parse = self.requests(self.competition(url)) + odds_to_merge = {} # parse odds - to_format = dict() - for parser, data in to_parse.items(): - to_format[parser]= getattr(self, parser)(data) # why not "self.parser(data)" ?? - - # TODO format odds - # get list of events - odds = to_format[list(to_parse.keys())[0]].copy() - for ind, event in enumerate(odds): - for parser in to_parse.keys(): - # add market (parser) to event - event[parser] = to_format[parser][ind].get('odds') - # pop inherited odds key - event.pop('odds') - odds[ind] = event.copy() + for parser, data in odds_to_parse.items(): + odds_to_merge[parser] = getattr(self, parser)(data) + + # collect events from full_time_result + odds = [ + { + 'time': e['time'], + 'home_team': e['home_team'], + 'away_team': e['away_team'], + } + for e in odds_to_merge['full_time_result'] + ] + + for category, events in odds_to_merge.items(): + for i, event in enumerate(events): + assert event['home_team'] == odds[i]['home_team'] + assert event['away_team'] == odds[i]['away_team'] + odds[i][category] = event['odds'] return odds From ea81ca9a666d8ddaa44187981e86264b856bd3e9 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Mon, 25 Jan 2021 01:40:21 +0100 Subject: [PATCH 14/14] remove useless import --- soccerapi/api/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/soccerapi/api/base.py b/soccerapi/api/base.py index 61d8b85..7991a45 100644 --- a/soccerapi/api/base.py +++ b/soccerapi/api/base.py @@ -1,5 +1,4 @@ import abc -from pprint import pprint from typing import List, Tuple