import sys import glob import re import pypyodbc import logging as log from os import path, remove from bs4 import BeautifulSoup as bs4 __version__ = '1.0.1' class JFRBidding: # 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: round_no = sitting[2] table_no = str(sitting[0]) + '_' + str(sitting[1]) if round_no not in round_lineups: round_lineups[round_no] = {} round_lineups[round_no][table_no] = sorted([ sitting[3], sitting[4]]) 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: 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) if len(bids[board_no][table_no][round_no]) == 0: bids[board_no][table_no].pop(round_no, None) else: bids[board_no][table_no][round_no][bid_counter] = { 'direction': bid[6], 'bid': bid[7]} return bids # converts bidding data into HTML table def __format_bidding(self, bidding): bid_match = re.compile('(\d)([SHDCN])') html_output = bs4('', 'lxml') header_row = html_output.new_tag('tr') html_output.table.append(header_row) for direction in self.__directions: header_cell = html_output.new_tag('th') header_cell.string = direction header_row.append(header_cell) for bid_round in bidding: bidding_row = html_output.new_tag('tr') html_output.table.append(bidding_row) for bid in bid_round: bid_cell = html_output.new_tag('td') call_match = re.match(bid_match, bid) if call_match: bid_cell.append(call_match.group(1)) bid_icon = html_output.new_tag( 'img', src='images/' + call_match.group(2) + '.gif') bid_cell.append(bid_icon) else: bid_cell.append(bid) bidding_row.append(bid_cell) 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): 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 def __map_board_numbers(self): self.__tournament_files = [ f for f in glob.glob(self.__tournament_prefix + '*.html') if re.search(self.__tournament_files_match, f)] for round_data in self.__lineup_data: # 13th column has JFR number for the first board if len(round_data) > 12: jfr_number = round_data[12] round_no = round_data[2] sector_no = round_data[0] table_no = round_data[1] if jfr_number and round_no: # 5th and 6th - actual board number for board_number in range(int(round_data[5]), int(round_data[6])+1): board_string = '_'.join([ str(board_number), str(round_no), str(sector_no), str(table_no)]) self.__board_number_mapping[ board_string ] = jfr_number + board_number - round_data[5] # only include these board numbers from mapping # which actually exist in JFR output custom_files = [] for b_number, jfr_number in self.__board_number_mapping.iteritems(): board_files = [ f for f in self.__tournament_files if f.endswith('{0:03}.html'.format(jfr_number))] if len(board_files): custom_files = custom_files + board_files else: self.__board_number_mapping[b_number] = None self.__tournament_files = custom_files # sitting read from BWS __round_lineups = {} # bidding read from BWS __bids = {} # full path + JFR prefix __tournament_prefix = '' # RegEx matching board HTML files __tournament_files_match = None # matched files, including board number mapping boundaries __tournament_files = [] # BWS number -> JFR number mapping __board_number_mapping = {} # all generated bidding table files, for cleanup purposes __bidding_files = [] def __init__(self, bws_file, file_prefix): with pypyodbc.win_connect_mdb(bws_file) as connection: cursor = connection.cursor() self.__lineup_data = cursor.execute( 'SELECT * FROM RoundData').fetchall() bid_data = cursor.execute('SELECT * FROM BiddingData').fetchall() self.__round_lineups = self.__parse_lineup_data(self.__lineup_data) self.__bids = self.__parse_bidding_data(bid_data) self.__tournament_prefix = path.splitext( path.realpath(file_prefix))[0] self.__tournament_files_match = re.compile( re.escape(self.__tournament_prefix) + '([0-9]{3})\.html') self.__map_board_numbers() def write_bidding_tables(self): 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'] 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)) bidding_fpath = \ self.__get_bidding_file_output_path( self.__board_number_mapping[board_no], round_no, table_no) self.__bidding_files.append(bidding_fpath) with file(bidding_fpath, 'w') as bidding_file: bidding_file.write( self.__format_bidding(bidding_table)) def write_bidding_scripts(self): for tournament_file in self.__tournament_files: with file(tournament_file, 'r+') as board_html: board_content = bs4(board_html, 'lxml', from_encoding='utf-8') header_scripts = board_content.select('head script') # check for jQuery, append if necessary jquery_scripts = [script for script in header_scripts if script['src'] == 'javas/jquery.js'] if not len(jquery_scripts): jquery = board_content.new_tag( 'script', src='javas/jquery.js', type='text/javascript') jquery_scripts.append(jquery) board_content.head.append(jquery) # check for bidding.js bidding_scripts = [ script for script in header_scripts if script['src'] == 'javas/bidding.js'] # and make sure bidding.js is appended after jQuery for script in bidding_scripts: script.extract() bidding_script = board_content.new_tag( 'script', src='javas/bidding.js', type='text/javascript') jquery_scripts[0].insert_after(bidding_script) board_html.seek(0) board_html.write(board_content.prettify( 'utf-8', formatter='html')) board_html.truncate() def write_bidding_links(self): for tournament_file in self.__tournament_files: file_number = re.match( self.__tournament_files_match, tournament_file).group(1) board_text_path = path.splitext(tournament_file)[0] + '.txt' 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') # 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])]) except ValueError: 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) if bidding_path in self.__bidding_files: del self.__bidding_files[ self.__bidding_files.index(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'])): # fourth cell is the contract for link in cells[3].select('a.biddingLink'): link.extract() cells[3].append(bidding_link) 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: remove(unused_file) if __name__ == '__main__': import argparse argument_parser = argparse.ArgumentParser( description='Display bidding data from BWS files on JFR Pary pages') def file_path(filepath): filepath = unicode(filepath, sys.getfilesystemencoding()) if path.isfile(filepath): return filepath else: argument_parser.error('File %s does not exist' % filepath) argument_parser.add_argument('-V', '--version', action='version', version='%(prog)s {version}'.format( version=__version__)) argument_parser.add_argument('bws_file', metavar='BWS_FILE', help='path to BWS file', type=file_path) argument_parser.add_argument('path', metavar='PATH', help='tournament path (to PREFIX.html)', type=file_path) console_output_args = argument_parser.add_mutually_exclusive_group() console_output_args.add_argument('-q', '--quiet', action='store_true', help='suppress warning on STDERR') console_output_args.add_argument('-v', '--verbose', action='store_true', help='be verbose on STDERR') argument_parser.add_argument('-l', '--log-level', metavar='LEVEL', help='file logging verbosity level', default='INFO', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']) argument_parser.add_argument('-f', '--log-file', metavar='LOGFILE', help='log file path', default='bidding_data.log') arguments = argument_parser.parse_args() # primary logging facility - virtual_table.log file log.basicConfig( level=getattr(log, arguments.log_level), format='%(asctime)s %(levelname)-8s %(name)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S', filename=arguments.log_file) # secondary logging facility - standard error output console_log = log.StreamHandler() console_log.setLevel(log.INFO if arguments.verbose else ( log.ERROR if arguments.quiet else log.WARNING)) console_log.setFormatter(log.Formatter( '%(levelname)-8s %(name)-8s: %(message)s')) log.getLogger().addHandler(console_log) bidding_parser = JFRBidding( bws_file=arguments.bws_file, file_prefix=arguments.path, ) bidding_parser.write_bidding_tables() bidding_parser.write_bidding_scripts() bidding_parser.write_bidding_links()