diff options
author | emkael <emkael@tlen.pl> | 2015-09-26 11:40:51 +0200 |
---|---|---|
committer | emkael <emkael@tlen.pl> | 2015-09-26 11:40:51 +0200 |
commit | 02c0145516cb365cce7a8fc8a60ce31488be0e9f (patch) | |
tree | 776c1519f2c5b83a1e0e493054b4ee0d53b61299 /src/bidding_data.py | |
parent | db9d70c8ca85723756058e7f606a2c51d6525c57 (diff) |
* 10.00/10 pylint compatibility
Diffstat (limited to 'src/bidding_data.py')
-rw-r--r-- | src/bidding_data.py | 392 |
1 files changed, 230 insertions, 162 deletions
diff --git a/src/bidding_data.py b/src/bidding_data.py index 3c7d8f7..ca92f0e 100644 --- a/src/bidding_data.py +++ b/src/bidding_data.py @@ -1,3 +1,8 @@ +""" Bidding data for JFR Pary result pages. +Utility to insert HTML tables with bidding data into traveller files generated +by JFR Pary. +""" + import sys import glob import re @@ -10,76 +15,96 @@ from bs4 import BeautifulSoup as bs4 __version__ = '1.0.1' -class JFRBidding: +def parse_lineup_data(sitting_data): + """ Converts BWS lineup to dictionary structure. + Structure: {round}.{sector}_{table}.{pair numbers} + """ + round_lineups = {} + for sitting in sitting_data: + log.getLogger('lineup').debug(sitting) + round_no = sitting[2] if sitting[2] is not None else 0 + table_no = str(sitting[0]) + '_' + str(sitting[1]) + lineup = sorted([sitting[3], sitting[4]]) + if round_no not in round_lineups: + round_lineups[round_no] = {} + round_lineups[round_no][table_no] = lineup + log.getLogger('lineup').debug('round %d, table %s: %s', + round_no, table_no, lineup) + return round_lineups + + +def parse_bidding_data(bidding_data): + """ Converts BWS bidding to dictionary structure. + Keys: {board}_{round}_{sector}_{table}.{sector}_{table}.{round} + Values: {bidding}[] + Applies erased calls + """ + bids = {} + for bid in bidding_data: + log.getLogger('bidding').debug(bid) + round_no = bid[3] + table_no = str(bid[1]) + '_' + str(bid[2]) + board_no = str(bid[4]) + '_' + str(round_no) + '_' + table_no + bid_counter = bid[5] + bid_erased = bid[10] + if board_no not in bids: + bids[board_no] = {} + if table_no not in bids[board_no]: + bids[board_no][table_no] = {} + if round_no not in bids[board_no][table_no]: + bids[board_no][table_no][round_no] = {} + if bid_erased == 1: + if bid_counter in bids[board_no][table_no][round_no]: + if bids[board_no][table_no][round_no][bid_counter][ + 'direction'] == bid[6]: + bids[board_no][table_no][round_no].pop( + bid_counter, None) + log.getLogger('bidding').debug( + 'erased bid %d from board %s, ' + + 'round %s, table %s-%s', + bid_counter, *board_no.split('_')) + if len(bids[board_no][table_no][round_no]) == 0: + bids[board_no][table_no].pop(round_no, None) + log.getLogger('bidding').debug( + 'bidding on board %s, round %s, ' + + 'table %s-%s empty, removing', + *board_no.split('_')) + else: + log.getLogger('bidding').debug( + 'bid does not match, not removing') + else: + bids[board_no][table_no][round_no][bid_counter] = { + 'direction': bid[6], 'bid': bid[7]} + log.getLogger('bidding').debug( + 'board %s, round %s, table %s-%s, bid %d: %s by %s', + *(board_no.split('_') + [bid_counter, bid[7], bid[6]])) + return bids + +def get_dealer(bidding): + """ Returns first player to call in a bidding. + """ + return bidding[min(bidding.keys())]['direction'] + +def get_last_bidder(bidding): + """ Returns last player to call in a bidding. + """ + return bidding[max(bidding.keys())]['direction'] + + + + +class JFRBidding(object): + """ Bidding data converter (from BWS data to JFR HTML pages) + """ # alignment of the bidding table __directions = ['W', 'N', 'E', 'S'] - # converts BWS lineup data to - # {round}.{sector}_{table}.{pair numbers} structure - def __parse_lineup_data(self, sitting_data): - round_lineups = {} - for sitting in sitting_data: - log.getLogger('lineup').debug(sitting) - round_no = sitting[2] if sitting[2] is not None else 0 - table_no = str(sitting[0]) + '_' + str(sitting[1]) - lineup = sorted([sitting[3], sitting[4]]) - if round_no not in round_lineups: - round_lineups[round_no] = {} - round_lineups[round_no][table_no] = lineup - log.getLogger('lineup').debug('round %d, table %s: %s', - round_no, table_no, lineup) - return round_lineups - - # converts BWS bidding to the structure: - # {board}_{round}_{sector}_{table}.{sector}_{table}.{round} -> {bidding}[], - # including erased calls - def __parse_bidding_data(self, bidding_data): - bids = {} - for bid in bidding_data: - log.getLogger('bidding').debug(bid) - round_no = bid[3] - table_no = str(bid[1]) + '_' + str(bid[2]) - board_no = str(bid[4]) + '_' + str(round_no) + '_' + table_no - bid_counter = bid[5] - bid_erased = bid[10] - if board_no not in bids: - bids[board_no] = {} - if table_no not in bids[board_no]: - bids[board_no][table_no] = {} - if round_no not in bids[board_no][table_no]: - bids[board_no][table_no][round_no] = {} - if bid_erased == 1: - if bid_counter in bids[board_no][table_no][round_no]: - if bids[board_no][table_no][round_no][bid_counter][ - 'direction'] == bid[6]: - bids[board_no][table_no][round_no].pop( - bid_counter, None) - log.getLogger('bidding').debug( - 'erased bid %d from board %s, ' + - 'round %s, table %s-%s', - bid_counter, *board_no.split('_')) - if len(bids[board_no][table_no][round_no]) == 0: - bids[board_no][table_no].pop(round_no, None) - log.getLogger('bidding').debug( - 'bidding on board %s, round %s, ' + - 'table %s-%s empty, removing', - *board_no.split('_')) - else: - log.getLogger('bidding').debug( - 'bid does not match, not removing') - else: - bids[board_no][table_no][round_no][bid_counter] = { - 'direction': bid[6], 'bid': bid[7]} - log.getLogger('bidding').debug( - 'board %s, round %s, table %s-%s, bid %d: %s by %s', - *(board_no.split('_') + [bid_counter, bid[7], bid[6]])) - return bids - - # converts bidding data into HTML table def __format_bidding(self, bidding): + """ Converts bidding data to properly formatted HTML table. + """ log.getLogger('b_format').debug('formatting bidding: %s', bidding) - bid_match = re.compile('(\d)([SHDCN])') + bid_match = re.compile(r'(\d)([SHDCN])') html_output = bs4('<table>', 'lxml') header_row = html_output.new_tag('tr') html_output.table.append(header_row) @@ -104,21 +129,25 @@ class JFRBidding: log.getLogger('b_format').debug('%5s' * 4, *bid_round) return html_output.table.prettify() - # returns file path for bidding HTML output - # {prefix}_bidding_{jfr_board_number}_{pair_numbers}.txt def __get_bidding_file_output_path(self, board_no, round_no=None, table_no=None, pair_numbers=None): + """ Compiles file path for bidding data (HTML file with bidding). + Path format: {prefix}_bidding_{jfr_board_number}_{pair_numbers}.txt + """ + if pair_numbers is None: + # read numbers from lineup + pair_numbers = self.__round_lineups[round_no][table_no] return u'{0}_bidding_{1:03}_{2}.txt'.format( self.__tournament_prefix, board_no, - '_'.join(map(str, - self.__round_lineups[round_no][table_no] - if pair_numbers is None # read numbers from lineup - else pair_numbers))) # or use provided numbers + '_'.join([str(num) for num in pair_numbers])) def __map_board_numbers(self): + """ Maps BWS board numbers to JFR board numbers. + Filters boards to these present in both sets of data. + """ self.__tournament_files = [ f for f in glob.glob(self.__tournament_prefix + '*.html') @@ -166,6 +195,100 @@ class JFRBidding: b_number, jfr_number) self.__tournament_files = list(set(custom_files)) + def __compile_bidding(self, bidding): + """ Compiles two-dimensional bidding table from a list of calls. + """ + bidding_table = [[], [], [], []] + # compile bidding player-by-player + for bid_index in sorted(bidding): + bid = bidding[bid_index] + bidding_table[ + self.__directions.index( + bid['direction']) + ].append(bid['bid']) + return bidding_table + + def __form_bidding(self, bidding_table, dealer, last_bidder): + """ Formats bidding table in equally sized, full rows. + """ + # fill skipped calls for players before dealer + # in the first round of bidding + for pos in range( + 0, self.__directions.index(dealer)): + bidding_table[pos].insert(0, '') + # fill skipped calls for players after pass out + # (so that bidding table is a proper matrix) + for pos in range( + self.__directions.index(last_bidder), + len(self.__directions)): + bidding_table[pos].append('') + # transpose the bidding table + # aligning it row-by-row (bid round-by-round) + return [list(row) for row in zip(*bidding_table)] + + def __write_bidding_file(self, board_text_path, file_number): + """ Alters traveller file to include links to bidding tables. + """ + with file(board_text_path, 'r+') as board_text: + board_text_content = bs4( + board_text, 'lxml') + used_files = [] + for row in board_text_content.select('tr'): + cells = row.select('td') + debug_string = ' '.join([ + ''.join([ + cc for cc + in c.contents if isinstance(cc, basestring)]).strip() + for c in cells]) + log.getLogger('links').debug('row: %s', debug_string) + # traveller table rows for specific score entries + # should have 11 cells + if len(cells) == 11: + try: + pair_numbers = sorted([ + int(cells[1].contents[0]), + int(cells[2].contents[0])]) + log.getLogger('links').debug( + 'pairs: %s', pair_numbers) + except ValueError: + log.getLogger('links').debug( + 'invalid pair numbers, skipping') + continue + bidding_link = board_text_content.new_tag( + 'a', href='#', **{'class': 'biddingLink'}) + bidding_link.string = ' ' + bidding_path = self.__get_bidding_file_output_path( + int(file_number, 10), + pair_numbers=pair_numbers) + bidding_link['data-bidding-link'] = path.basename( + bidding_path) + # only append link if we've got bidding data + if path.isfile(path.join( + path.dirname(self.__tournament_prefix), + bidding_link['data-bidding-link'])): + if bidding_path in self.__bidding_files: + used_files.append(bidding_path) + log.getLogger('links').info( + 'linking: %s', + bidding_link['data-bidding-link']) + # fourth cell is the contract + for link in cells[3].select('a.biddingLink'): + log.getLogger('links').debug( + 'removing existing link') + link.extract() + cells[3].append(bidding_link) + else: + log.getLogger('links').warning( + 'bidding for file path %s not found', + bidding_link['data-bidding-link']) + else: + log.getLogger('links').debug('skipping row') + board_text.seek(0) + board_text.write(board_text_content.table.prettify( + 'utf-8', formatter='html')) + board_text.truncate() + return used_files + # sitting read from BWS __round_lineups = {} # bidding read from BWS @@ -193,56 +316,39 @@ class JFRBidding: bid_data = cursor.execute('SELECT * FROM BiddingData').fetchall() log.getLogger('init').debug('parsing lineup data (%d entries)', len(self.__lineup_data)) - self.__round_lineups = self.__parse_lineup_data(self.__lineup_data) + self.__round_lineups = parse_lineup_data(self.__lineup_data) log.getLogger('init').debug('parsing bidding data (%d entries)', len(bid_data)) - self.__bids = self.__parse_bidding_data(bid_data) + self.__bids = parse_bidding_data(bid_data) log.getLogger('init').debug('parsing prefix, filename = %s', file_prefix) self.__tournament_prefix = path.splitext( path.realpath(file_prefix))[0] log.getLogger('init').debug('prefix = %s', self.__tournament_prefix) self.__tournament_files_match = re.compile( - re.escape(self.__tournament_prefix) + '([0-9]{3})\.html') + re.escape(self.__tournament_prefix) + r'([0-9]{3})\.html') log.getLogger('init').debug('tournament files pattern: %s', self.__tournament_files_match.pattern) self.__map_board_numbers() def write_bidding_tables(self): + """ Iterates over bidding and writes tables to HTML files. + """ for board_no, board_data in self.__bids.items(): if board_no in self.__board_number_mapping: for table_no, table_data in board_data.items(): for round_no, round_data in table_data.items(): if round_no in self.__round_lineups: if table_no in self.__round_lineups[round_no]: - bidding = sorted(round_data) - dealer = round_data[bidding[0]]['direction'] + dealer = get_dealer(round_data) log.getLogger('tables').debug( 'board %s: %d bids, dealer %s', - board_no, len(bidding), dealer) - bidding_table = [[], [], [], []] - # compile bidding player-by-player - for bid_index in bidding: - bid = round_data[bid_index] - bidding_table[ - self.__directions.index( - bid['direction']) - ].append(bid['bid']) - last_bidder = bid['direction'] - # fill skipped calls for players before dealer - # in the first round of bidding - for pos in range( - 0, self.__directions.index(dealer)): - bidding_table[pos].insert(0, '') - # fill skipped calls for players after pass out - # (so that bidding table is a proper matrix) - for pos in range( - self.__directions.index(last_bidder), - len(self.__directions)): - bidding_table[pos].append('') - # transpose the bidding table - # aligning it row-by-row (bid round-by-round) - bidding_table = map(list, zip(*bidding_table)) + board_no, len(round_data), dealer) + last_bidder = get_last_bidder(round_data) + bidding_table = self.__compile_bidding( + round_data) + bidding_table = self.__form_bidding( + bidding_table, dealer, last_bidder) log.getLogger('tables').debug( 'compiled into %d rounds of bidding', len(bidding_table)) @@ -271,6 +377,8 @@ class JFRBidding: board_no) def write_bidding_scripts(self): + """ Alters traveller files to include necessary JavaScript. + """ for tournament_file in self.__tournament_files: log.getLogger('scripts').info('writing scripts into: %s', tournament_file) @@ -306,6 +414,10 @@ class JFRBidding: board_html.truncate() def write_bidding_links(self): + """ Iterates over traveller files to include links to bidding tables. + Cleans up bidding table files, which are not used. + """ + used_bidding_tables = [] for tournament_file in self.__tournament_files: file_number = re.match( self.__tournament_files_match, @@ -313,76 +425,28 @@ class JFRBidding: board_text_path = path.splitext(tournament_file)[0] + '.txt' log.getLogger('links').info('writing traveller for board %s: %s', file_number, board_text_path) - with file(board_text_path, 'r+') as board_text: - board_text_content = bs4( - board_text, 'lxml') - for row in board_text_content.select('tr'): - cells = row.select('td') - debug_string = ' '.join(map( - lambda c: ''.join(filter( - lambda cc: isinstance(cc, basestring), - c.contents)).strip(), - cells)) - log.getLogger('links').debug('row: %s', debug_string) - # traveller table rows for specific score entries - # should have 11 cells - if len(cells) == 11: - try: - pair_numbers = sorted([ - int(cells[1].contents[0]), - int(cells[2].contents[0])]) - log.getLogger('links').debug( - 'pairs: %s', pair_numbers) - except ValueError: - log.getLogger('links').debug( - 'invalid pair numbers, skipping') - continue - bidding_link = board_text_content.new_tag( - 'a', href='#', **{'class': 'biddingLink'}) - bidding_link.string = ' ' - bidding_path = self.__get_bidding_file_output_path( - int(file_number, 10), - pair_numbers=pair_numbers) - bidding_link['data-bidding-link'] = path.basename( - bidding_path) - # only append link if we've got bidding data - if path.isfile(path.join( - path.dirname(self.__tournament_prefix), - bidding_link['data-bidding-link'])): - if bidding_path in self.__bidding_files: - del self.__bidding_files[ - self.__bidding_files.index(bidding_path)] - log.getLogger('links').info( - 'linking: %s', - bidding_link['data-bidding-link']) - # fourth cell is the contract - for link in cells[3].select('a.biddingLink'): - log.getLogger('links').debug( - 'removing existing link') - link.extract() - cells[3].append(bidding_link) - else: - log.getLogger('links').warning( - 'bidding for file path %s not found', - bidding_link['data-bidding-link']) - else: - log.getLogger('links').debug('skipping row') - board_text.seek(0) - board_text.write(board_text_content.table.prettify( - 'utf-8', formatter='html')) - board_text.truncate() - for unused_file in self.__bidding_files: + used_bidding_tables = self.__write_bidding_file( + board_text_path, file_number) + used_bidding_tables + log.getLogger('links').info('used board files: %s', + ', '.join(used_bidding_tables)) + for unused_file in [unused for unused + in self.__bidding_files + if unused not in used_bidding_tables]: log.getLogger('links').warning( 'bidding file %s not used, deleting', unused_file) remove(unused_file) -if __name__ == '__main__': +def main(): + """ Program entry point, invoked when __name__ is __main__ + """ import argparse argument_parser = argparse.ArgumentParser( description='Display bidding data from BWS files on JFR Pary pages') def file_path(filepath): + """ Sanitizes and validates file paths from input parameters. + """ filepath = unicode(filepath, sys.getfilesystemencoding()) if path.isfile(filepath): return filepath @@ -391,7 +455,7 @@ if __name__ == '__main__': argument_parser.add_argument('-V', '--version', action='version', version='%(prog)s {version}'.format( - version=__version__)) + version=__version__)) argument_parser.add_argument('bws_file', metavar='BWS_FILE', help='path to BWS file', @@ -445,7 +509,11 @@ if __name__ == '__main__': bidding_parser.write_bidding_tables() bidding_parser.write_bidding_scripts() bidding_parser.write_bidding_links() - except Exception as e: - log.getLogger('root').error(e.strerror) + except Exception as ex: + log.getLogger('root').error(ex.strerror) + raise log.info('--------- program ended ---------') + +if __name__ == '__main__': + main() |