summaryrefslogtreecommitdiff
path: root/jfr_playoff/data
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2019-12-30 12:49:54 +0100
committeremkael <emkael@tlen.pl>2019-12-30 12:49:54 +0100
commit7a598f65372b1b694d222946fd6269033bde0e54 (patch)
treee33fb74aad63756f0b3c94f12e29aff6c97064ed /jfr_playoff/data
parentf77bac9b5406c6bf6b1c819f155164568ef4af36 (diff)
New package structure for result info classes
Diffstat (limited to 'jfr_playoff/data')
-rw-r--r--jfr_playoff/data/__init__.py207
-rw-r--r--jfr_playoff/data/info.py29
-rw-r--r--jfr_playoff/data/tournament/__init__.py58
-rw-r--r--jfr_playoff/data/tournament/jfrdb.py60
-rw-r--r--jfr_playoff/data/tournament/jfrhtml.py92
-rw-r--r--jfr_playoff/data/tournament/tcjson.py62
6 files changed, 508 insertions, 0 deletions
diff --git a/jfr_playoff/data/__init__.py b/jfr_playoff/data/__init__.py
new file mode 100644
index 0000000..494eaef
--- /dev/null
+++ b/jfr_playoff/data/__init__.py
@@ -0,0 +1,207 @@
+from cached_property import cached_property
+
+from jfr_playoff.db import PlayoffDB
+from jfr_playoff.dto import Phase
+from jfr_playoff.matchinfo import MatchInfo
+from jfr_playoff.data.tournament import TournamentInfo
+from jfr_playoff.logger import PlayoffLogger
+
+
+class PlayoffData(object):
+ def __init__(self, settings=None):
+ if settings is not None:
+ self.database = PlayoffDB(settings.get('database')) \
+ if settings.has_section('database') else None
+ if self.database is None:
+ PlayoffLogger.get('db').warning(
+ PlayoffDB.DATABASE_NOT_CONFIGURED_WARNING)
+ self.team_settings = settings.get('teams')
+ self.custom_final_order = []
+ if settings.has_section('custom_final_order'):
+ self.custom_final_order = settings.get('custom_final_order')
+ self.custom_final_order = [
+ t for t in [
+ self.teams[i-1] if isinstance(i, int)
+ else self.get_team_data_by_name(i)
+ for i in self.custom_final_order]
+ if t is not None]
+ self.phases = settings.get('phases')
+ self.swiss = []
+ if settings.has_section('swiss'):
+ self.swiss = settings.get('swiss')
+ self.aliases = {}
+ if settings.has_section('team_aliases'):
+ self.aliases = settings.get('team_aliases')
+ self.grid = []
+ self.match_info = {}
+ self.leaderboard = []
+
+ def fetch_team_list(self, settings, db_interface):
+ if isinstance(settings, list):
+ PlayoffLogger.get('data').info(
+ 'team list pre-defined: %s', settings)
+ return settings
+ tournament_info = TournamentInfo(settings, db_interface)
+ team_list = tournament_info.get_tournament_results()
+ if len(team_list) == 0:
+ PlayoffLogger.get('data').warning('team list is empty!')
+ return team_list if 'max_teams' not in settings \
+ else team_list[0:settings['max_teams']]
+
+ @cached_property
+ def teams(self):
+ return self.fetch_team_list(self.team_settings, self.database)
+
+ def generate_phases(self):
+ self.grid = []
+ for phase in self.phases:
+ dummies = phase.get('dummies', [])
+ phase_count = len(phase['matches']) + len(dummies)
+ phase_object = Phase()
+ phase_object.title = phase['title']
+ phase_object.link = phase.get('link', None)
+ phase_object.matches = [None] * phase_count
+ phase_pos = 0
+ for match in phase['matches']:
+ while phase_pos in dummies:
+ phase_pos += 1
+ phase_object.matches[phase_pos] = match['id']
+ phase_pos += 1
+ PlayoffLogger.get('data').info('phase object: %s', phase_object)
+ self.grid.append(phase_object)
+ return self.grid
+
+ def fill_match_info(self):
+ self.match_info = {}
+ for phase in self.phases:
+ for match in phase['matches']:
+ match_info = MatchInfo(match, self.teams, self.database, self.aliases)
+ if 'link' in phase:
+ match_info.set_phase_link(phase['link'])
+ self.match_info[match['id']] = match_info.get_info()
+ if self.match_info[match['id']].running > 0:
+ for phase_obj in self.grid:
+ if match['id'] in phase_obj.matches:
+ phase_obj.running = True
+ PlayoffLogger.get('data').info(
+ 'match object: %s', self.match_info[match['id']])
+ return self.match_info
+
+ def get_swiss_link(self, event):
+ event_info = TournamentInfo(event, self.database)
+ swiss_link = event_info.get_results_link()
+ if event.get('relative_path', None):
+ swiss_link = '%s/%s' % (event['relative_path'], swiss_link)
+ PlayoffLogger.get('data').info('swiss link: %s', swiss_link)
+ return swiss_link
+
+ def prefill_leaderboard(self, teams):
+ self.leaderboard = [None] * len(teams)
+ for team in teams:
+ if len(team) > 3 and team[3] is not None:
+ self.leaderboard[team[3]-1] = team[0]
+ self.fill_swiss_leaderboard(self.swiss, teams)
+ PlayoffLogger.get('data').info('leaderboard pre-filled: %s', self.leaderboard)
+ return self.leaderboard
+
+ def fill_swiss_leaderboard(self, swiss, teams):
+ teams = [team[0] for team in teams]
+ for event in swiss:
+ event['ties'] = teams
+ event_info = TournamentInfo(event, self.database)
+ if event_info.is_finished():
+ swiss_position = event.get('swiss_position', 1)
+ position_limit = event.get('position_to', 9999)
+ place = 1
+ swiss_results = event_info.get_tournament_results()
+ for team in swiss_results:
+ if place >= swiss_position:
+ target_position = event['position'] \
+ + place - swiss_position
+ if target_position <= min(
+ position_limit, len(self.leaderboard)):
+ self.leaderboard[
+ target_position - 1] = team[0]
+ place += 1
+ PlayoffLogger.get('data').info(
+ 'leaderboard after %s swiss: %s', event, self.leaderboard)
+
+ def fill_leaderboard(self):
+ self.prefill_leaderboard(self.teams)
+ leaderboard_teams = {}
+ for phase in self.phases:
+ for match in phase['matches']:
+ if 'winner' in match:
+ winner_key = tuple(match['winner'])
+ if winner_key not in leaderboard_teams:
+ leaderboard_teams[winner_key] = []
+ leaderboard_teams[winner_key].append(
+ self.match_info[match['id']].winner)
+ if 'loser' in match:
+ loser_key = tuple(match['loser'])
+ if loser_key not in leaderboard_teams:
+ leaderboard_teams[loser_key] = []
+ leaderboard_teams[loser_key].append(
+ self.match_info[match['id']].loser)
+ final_order = self.custom_final_order + [
+ t for t in self.teams if t not in self.custom_final_order]
+ PlayoffLogger.get('data').info(
+ 'custom order for final positions: %s', self.custom_final_order)
+ PlayoffLogger.get('data').info(
+ 'order of teams to fill leaderboard positions: %s',
+ final_order)
+ for positions, position_teams in leaderboard_teams.iteritems():
+ positions = list(positions)
+ PlayoffLogger.get('data').info(
+ 'filling leaderboard positions %s with teams %s',
+ positions, position_teams)
+ if len(positions) == len([
+ team for team in position_teams if team is not None]):
+ for table_team in final_order:
+ if table_team[0] in position_teams:
+ position = positions.pop(0)
+ self.leaderboard[position-1] = table_team[0]
+ PlayoffLogger.get('data').info(
+ 'team %s in position %d', table_team[0], position)
+ PlayoffLogger.get('data').info(
+ 'leaderboard filled: %s', self.leaderboard)
+ return self.leaderboard
+
+ def get_swiss_info(self):
+ swiss_info = [{
+ 'link': self.get_swiss_link(event),
+ 'position': event['position'],
+ 'label': event.get('label', None),
+ 'finished': TournamentInfo(event, self.database).is_finished()
+ } for event in self.swiss]
+ PlayoffLogger.get('data').info('swiss info: %s', swiss_info)
+ return swiss_info
+
+ def get_dimensions(self):
+ dimensions = (
+ len(self.phases),
+ max([
+ len(phase['matches']) + len(phase.get('dummies', []))
+ for phase in self.phases
+ ] or [0])
+ )
+ PlayoffLogger.get('data').info('grid dimensions: %s', dimensions)
+ return dimensions
+
+ def get_team_data_by_name(self, fullname):
+ for team in self.teams:
+ if team[0] == fullname:
+ return team
+ return None
+
+ def get_shortname(self, fullname):
+ for team in self.teams:
+ if team[0] == fullname:
+ return team[1]
+ return fullname
+
+ def get_team_image(self, fullname):
+ for team in self.teams:
+ if team[0] == fullname and len(team) > 2:
+ return team[2]
+ return None
diff --git a/jfr_playoff/data/info.py b/jfr_playoff/data/info.py
new file mode 100644
index 0000000..371ac6a
--- /dev/null
+++ b/jfr_playoff/data/info.py
@@ -0,0 +1,29 @@
+from jfr_playoff.logger import PlayoffLogger
+
+
+class ResultInfo(object):
+ def __init__(self, *args):
+ self.clients = self.fill_client_list(*args)
+
+ def fill_client_list(self, settings, database):
+ return []
+
+ def call_client(self, method, default, *args):
+ PlayoffLogger.get('resultinfo').info(
+ 'calling %s on result info clients', method)
+ for client in self.clients:
+ try:
+ ret = getattr(client, method)(*args)
+ PlayoffLogger.get('resultinfo').info(
+ '%s method returned %s', method, ret)
+ return ret
+ except Exception as e:
+ if type(e) in client.get_exceptions(method):
+ PlayoffLogger.get('resultinfo').warning(
+ '%s method raised %s(%s)',
+ method, type(e).__name__, str(e))
+ else:
+ raise
+ PlayoffLogger.get('resultinfo').info(
+ '%s method returning default: %s', method, default)
+ return default
diff --git a/jfr_playoff/data/tournament/__init__.py b/jfr_playoff/data/tournament/__init__.py
new file mode 100644
index 0000000..e86d294
--- /dev/null
+++ b/jfr_playoff/data/tournament/__init__.py
@@ -0,0 +1,58 @@
+from jfr_playoff.logger import PlayoffLogger
+from jfr_playoff.data.info import ResultInfo
+
+
+class TournamentInfoClient(object):
+ def __init__(self, settings, database=None):
+ self.settings = settings
+ self.database = database
+
+ def get_results_link(self, suffix):
+ pass
+
+ def is_finished(self):
+ pass
+
+ def get_tournament_results(self):
+ pass
+
+ def get_exceptions(self, method):
+ pass
+
+
+class TournamentInfo(ResultInfo):
+ def __init__(self, settings, database):
+ self.settings = settings
+ ResultInfo.__init__(self, settings, database)
+
+ def fill_client_list(self, settings, database):
+ clients = []
+ from jfr_playoff.data.tournament.jfrdb import JFRDbTournamentInfo
+ from jfr_playoff.data.tournament.jfrhtml import JFRHtmlTournamentInfo
+ from jfr_playoff.data.tournament.tcjson import TCJsonTournamentInfo
+ if (database is not None) and ('database' in settings):
+ clients.append(JFRDbTournamentInfo(settings, database))
+ if 'link' in settings:
+ if settings['link'].endswith('leaderb.html'):
+ clients.append(JFRHtmlTournamentInfo(settings))
+ clients.append(TCJsonTournamentInfo(settings))
+ return clients
+
+ def get_tournament_results(self):
+ teams = self.call_client('get_tournament_results', [])
+ if self.is_finished():
+ final_positions = self.settings.get('final_positions', [])
+ PlayoffLogger.get('tournamentinfo').info(
+ 'setting final positions from tournament results: %s',
+ final_positions)
+ for position in final_positions:
+ if len(teams) >= position:
+ teams[position-1] = (teams[position-1] + [None] * 4)[0:4]
+ teams[position-1][3] = position
+ return teams
+
+ def is_finished(self):
+ return self.call_client('is_finished', True)
+
+ def get_results_link(self, suffix='leaderb.html'):
+ return self.call_client('get_results_link', None, suffix)
diff --git a/jfr_playoff/data/tournament/jfrdb.py b/jfr_playoff/data/tournament/jfrdb.py
new file mode 100644
index 0000000..48645df
--- /dev/null
+++ b/jfr_playoff/data/tournament/jfrdb.py
@@ -0,0 +1,60 @@
+import jfr_playoff.sql as p_sql
+
+from jfr_playoff.logger import PlayoffLogger
+from jfr_playoff.data.tournament import TournamentInfoClient
+
+SWISS_TIE_WARNING = 'tie detected in swiss %s.' + \
+ ' Make sure to resolve the tie by arranging teams' + \
+ ' in configuration file.'
+
+
+class JFRDbTournamentInfo(TournamentInfoClient):
+ def get_exceptions(self, method):
+ return (IOError, TypeError, IndexError, KeyError)
+
+ def get_results_link(self, suffix='leaderb.html'):
+ row = self.database.fetch(
+ self.settings['database'], p_sql.PREFIX, ())
+ if row is not None:
+ if len(row) > 0:
+ link = row[0] + suffix
+ PlayoffLogger.get('tournament.jfrdb').info(
+ 'generating tournament-specific link from DB %s prefix: %s -> %s',
+ self.settings['database'], suffix, link)
+ return link
+ raise ValueError('unable to fetch db link')
+
+ def is_finished(self):
+ finished = self.database.fetch(
+ self.settings['database'], p_sql.SWISS_ENDED, {})
+ PlayoffLogger.get('tournament.jfrdb').info(
+ 'fetching tournament finished status from DB %s: %s',
+ self.settings['database'], finished)
+ return (len(finished) > 0) and (finished[0] > 0)
+
+ def get_tournament_results(self):
+ if 'ties' not in self.settings:
+ self.settings['ties'] = []
+ swiss_teams = self.database.fetch_all(
+ self.settings['database'], p_sql.SWISS_RESULTS, {})
+ swiss_results = sorted(
+ swiss_teams,
+ key=lambda t: self.settings['ties'].index(t[0]) \
+ if t[0] in self.settings['ties'] else -1)
+ swiss_results = sorted(
+ swiss_results, key=lambda t: t[1], reverse=True)
+ swiss_results = sorted(swiss_results, key=lambda team: team[2])
+ PlayoffLogger.get('tournament.jfrdb').info(
+ 'fetched tournament results from database %s: %s',
+ self.settings['database'], swiss_results)
+ prev_result = None
+ for team in swiss_results:
+ if prev_result == team[1]:
+ PlayoffLogger.get('tournament.jfrdb').warning(
+ SWISS_TIE_WARNING, self.settings['database'])
+ prev_result = team[1]
+ db_teams = [[team[0], team[3], team[4]] for team in swiss_results]
+ PlayoffLogger.get('tournament.jfrdb').info(
+ 'fetched team list from database %s: %s',
+ self.settings['database'], db_teams)
+ return db_teams
diff --git a/jfr_playoff/data/tournament/jfrhtml.py b/jfr_playoff/data/tournament/jfrhtml.py
new file mode 100644
index 0000000..017ce3b
--- /dev/null
+++ b/jfr_playoff/data/tournament/jfrhtml.py
@@ -0,0 +1,92 @@
+from math import ceil
+import re
+
+from jfr_playoff.logger import PlayoffLogger
+from jfr_playoff.remote import RemoteUrl as p_remote
+from jfr_playoff.data.tournament import TournamentInfoClient
+
+
+class JFRHtmlTournamentInfo(TournamentInfoClient):
+ def get_exceptions(self, method):
+ if method == 'get_results_link':
+ return (KeyError, ValueError)
+ return (TypeError, IndexError, KeyError, IOError, ValueError)
+
+ def get_results_link(self, suffix='leaderb.html'):
+ link = re.sub(r'leaderb.html$', suffix, self.settings['link'])
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'generating tournament-specific link from leaderboard link %s: %s -> %s',
+ self.settings['link'], suffix, link)
+ return link
+
+ def is_finished(self):
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'fetching tournament finished status from HTML: %s',
+ self.settings['link'])
+ leaderboard = p_remote.fetch(self.settings['link'])
+ leaderb_heading = leaderboard.select('td.bdnl12')[0].text
+ contains_digits = any(char.isdigit() for char in leaderb_heading)
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'tournament header from HTML: %s, %s',
+ leaderb_heading,
+ 'contains digits' if contains_digits else "doesn't contain digits")
+ non_zero_scores = [
+ imps.text
+ for imps
+ in leaderboard.select('td.bdc small')
+ if imps.text != '0-0']
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'tournament leaderboard from HTML: has %d non-zero scores',
+ len(non_zero_scores))
+ finished = (not contains_digits) and (len(non_zero_scores) > 0)
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'tournament leaderboard from HTML indicates finished: %s',
+ finished)
+ return finished
+
+ def get_tournament_results(self):
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'fetching tournament results from leaderboard URL: %s',
+ self.settings['link'])
+ leaderboard = p_remote.fetch(self.settings['link'])
+ result_links = [
+ row.select('a[onmouseover]')
+ for row
+ in leaderboard.find_all('tr')
+ if len(row.select('a[onmouseover]')) > 0]
+ results = [None] * (len(result_links) * max([
+ len(links) for links in result_links]))
+ for i in range(0, len(result_links)):
+ for j in range(0, len(result_links[i])):
+ results[len(result_links) * j + i] = result_links[i][j]
+ teams = []
+ team_links = {}
+ for team in results:
+ if team is not None:
+ team_info = []
+ fullname = team.text.strip(u'\xa0')
+ team_links[team['href']] = fullname
+ team_info.append(fullname)
+ team_info.append('')
+ team_image = team.find('img')
+ if team_image is not None:
+ team_info.append(team_image['src'].replace('images/', ''))
+ teams.append(team_info)
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'read tournament results from leaderboard: %s', teams)
+ for table in range(1, int(ceil(len(teams)/2.0))+1):
+ table_url = self.get_results_link('1t%d-1.html' % (table))
+ table_content = p_remote.fetch(table_url)
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'reading team shortnames from traveller: %s', table_url)
+ for link in table_content.select('a.br'):
+ if link['href'] in team_links:
+ for team in teams:
+ if team[0] == team_links[link['href']]:
+ team[1] = link.text.strip(u'\xa0')
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'shortname for %s: %s', team[0], team[1])
+ break
+ PlayoffLogger.get('tournament.jfrhtml').info(
+ 'tournament results from HTML: %s', teams)
+ return teams
diff --git a/jfr_playoff/data/tournament/tcjson.py b/jfr_playoff/data/tournament/tcjson.py
new file mode 100644
index 0000000..5e375ed
--- /dev/null
+++ b/jfr_playoff/data/tournament/tcjson.py
@@ -0,0 +1,62 @@
+import json
+import urlparse
+
+from jfr_playoff.logger import PlayoffLogger
+from jfr_playoff.remote import RemoteUrl as p_remote
+from jfr_playoff.data.tournament import TournamentInfoClient
+
+FLAG_CDN_URL = 'https://cdn.tournamentcalculator.com/flags/'
+
+
+class TCJsonTournamentInfo(TournamentInfoClient):
+ def get_exceptions(self, method):
+ return (TypeError, IndexError, KeyError, IOError, ValueError)
+
+ def get_results_link(self, suffix):
+ link = urlparse.urljoin(self.settings['link'], suffix)
+ PlayoffLogger.get('tournament.tcjson').info(
+ 'generating tournament-specific link from leaderboard link %s: %s -> %s',
+ self.settings['link'], suffix, link)
+ return link
+
+ def is_finished(self):
+ settings_json = json.loads(
+ p_remote.fetch_raw(self.get_results_link('settings.json')))
+ live_results = settings_json['LiveResults']
+ last_round = settings_json['LastPlayedRound']
+ last_session = settings_json['LastPlayedSession']
+ finished = (not live_results) \
+ and (last_round > 0) and (last_session > 0)
+ PlayoffLogger.get('tournament.tcjson').info(
+ 'tournament settings (live = %s, last_round = %d, last_session = %d) indicate finished: %s',
+ live_results, last_round, last_session, finished)
+ return finished
+
+ def get_tournament_results(self):
+ results = []
+ results_json = json.loads(
+ p_remote.fetch_raw(self.get_results_link('results.json')))
+ participant_groups = []
+ for result in results_json['Results']:
+ group = result['ParticipantGroup']
+ if group is not None:
+ if group not in participant_groups:
+ participant_groups.append(group)
+ group_id = participant_groups.index(group) + 1
+ else:
+ group_id = 999999
+ participant = result['Participant']
+ flag_url = None
+ flag = participant['_flag']
+ if flag is not None:
+ flag_url = self.get_results_link(
+ flag['CustomFlagPath']
+ if flag['IsCustom']
+ else '%s/%s.png' % (FLAG_CDN_URL, flag['CountryNameCode']))
+ results.append((
+ group_id, result['Place'],
+ participant['_name'], participant['_shortName'],
+ flag_url))
+ PlayoffLogger.get('tournament.tcjson').info(
+ 'tournament results fetched: %s' % results)
+ return [list(r[2:]) + [None] for r in sorted(results)]