diff --git a/pyfpdb/CarbonToFpdb.py b/pyfpdb/CarbonToFpdb.py index cc7afb81..0640d157 100644 --- a/pyfpdb/CarbonToFpdb.py +++ b/pyfpdb/CarbonToFpdb.py @@ -1,6 +1,7 @@ #!/usr/bin/env python -# Copyright 2008, Carl Gherardi - +# -*- coding: utf-8 -*- +# +# Copyright 2010, Matthew Boss # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,93 +19,286 @@ ######################################################################## -# Standard Library modules -import Configuration -import traceback +# This code is based heavily on EverleafToFpdb.py, by Carl Gherardi +# +# OUTSTANDING MATTERS +# +# -- No siteID assigned +# -- No support for games other than NL hold 'em cash. Hand histories for other +# games required +# -- No support for limit hold 'em yet, though this would be easy to add +# -- No support for tournaments (see also the last item below) +# -- Assumes that the currency of ring games is USD +# -- Only works for 'gametype="2"'. What is 'gametype'? +# -- Only accepts 'realmoney="true"' +# -- A hand's time-stamp does not record seconds past the minute (a +# limitation of the history format) +# -- No support for a bring-in or for antes (is the latter in fact unnecessary +# for hold 'em on Carbon?) +# -- hand.maxseats can only be guessed at +# -- The last hand in a history file will often be incomplete and is therefore +# rejected +# -- Is behaviour currently correct when someone shows an uncalled hand? +# -- Information may be lost when the hand ID is converted from the native form +# xxxxxxxx-yyy(y*) to xxxxxxxxyyy(y*) (in principle this should be stored as +# a string, but the database does not support this). Is there a possibility +# of collision between hand IDs that ought to be distinct? +# -- Cannot parse tables that run it twice (nor is this likely ever to be +# possible) +# -- Cannot parse hands in which someone is all in in one of the blinds. Until +# this is corrected tournaments will be unparseable + import sys -import re -import xml.dom.minidom -from xml.dom.minidom import Node -from HandHistoryConverter import HandHistoryConverter +import logging +from HandHistoryConverter import * +from decimal import Decimal -# Carbon format looks like: +class Carbon(HandHistoryConverter): -# 1) -# 2) -# 3) -# -# ... -# 4) -# -# -# 5) -# -# 6) -# -# .... -# + sitename = "Carbon" + filetype = "text" + codepage = "cp1252" + siteID = 11 -# The full sequence for a NHLE cash game is: -# BLINDS, PREFLOP, POSTFLOP, POSTTURN, POSTRIVER, SHOWDOWN, END_OF_GAME -# This sequence can be terminated after BLINDS at any time by END_OF_FOLDED_GAME + # Static regexes + re_SplitHands = re.compile(r'\n+(?=)') + re_GameInfo = re.compile(r'', re.MULTILINE) + re_HandInfo = re.compile(r'[0-9]+)">') + re_PlayerInfo = re.compile(r'', re.MULTILINE) + re_Board = re.compile(r'', re.MULTILINE) + re_PostBB = re.compile(r'', re.MULTILINE) + re_PostBoth = re.compile(r'', re.MULTILINE) + #re_Antes = ??? + #re_BringIn = ??? + re_HeroCards = re.compile(r'', re.MULTILINE) + re_ShowdownAction = re.compile(r'', re.MULTILINE) + re_CollectPot = re.compile(r'', re.MULTILINE) + re_ShownCards = re.compile(r'', re.MULTILINE) -class CarbonPoker(HandHistoryConverter): - def __init__(self, config, filename): - print "Initialising Carbon Poker converter class" - HandHistoryConverter.__init__(self, config, filename, "Carbon") # Call super class init - self.setFileType("xml") - self.siteId = 4 # Needs to match id entry in Sites database + def compilePlayerRegexs(self, hand): + pass - def readSupportedGames(self): - pass - def determineGameType(self): - gametype = [] - desc_node = self.doc.getElementsByTagName("description") - #TODO: no examples of non ring type yet - gametype = gametype + ["ring"] - type = desc_node[0].getAttribute("type") - if(type == "Holdem"): - gametype = gametype + ["hold"] - else: - print "Carbon: Unknown gametype: '%s'" % (type) + def playerNameFromSeatNo(self, seatNo, hand): + # This special function is required because Carbon Poker records + # actions by seat number, not by the player's name + for p in hand.players: + if p[0] == int(seatNo): + return p[1] - stakes = desc_node[0].getAttribute("stakes") - #TODO: no examples of anything except nlhe - m = re.match('(?PNo Limit)\s\(\$?(?P[.0-9]+)/\$?(?P[.0-9]+)\)', stakes) + def readSupportedGames(self): + return [["ring", "hold", "nl"], + ["tour", "hold", "nl"]] - if(m.group('LIMIT') == "No Limit"): - gametype = gametype + ["nl"] + def determineGameType(self, handText): + """return dict with keys/values: + 'type' in ('ring', 'tour') + 'limitType' in ('nl', 'cn', 'pl', 'cp', 'fl') + 'base' in ('hold', 'stud', 'draw') + 'category' in ('holdem', 'omahahi', omahahilo', 'razz', 'studhi', 'studhilo', 'fivedraw', '27_1draw', '27_3draw', 'badugi') + 'hilo' in ('h','l','s') + 'smallBlind' int? + 'bigBlind' int? + 'smallBet' + 'bigBet' + 'currency' in ('USD', 'EUR', 'T$', ) +or None if we fail to get the info """ - gametype = gametype + [self.float2int(m.group('SB'))] - gametype = gametype + [self.float2int(m.group('BB'))] + m = self.re_GameInfo.search(handText) + if not m: + # Information about the game type appears only at the beginning of + # a hand history file; hence it is not supplied with the second + # and subsequent hands. In these cases we use the value previously + # stored. + return self.info + self.info = {} + mg = m.groupdict() - return gametype + limits = { 'No Limit':'nl', 'Limit':'fl' } + games = { # base, category + 'Holdem' : ('hold','holdem'), + 'Holdem Tournament' : ('hold','holdem') } - def readPlayerStacks(self): - pass - def readBlinds(self): - pass - def readAction(self): - pass + if 'LIMIT' in mg: + self.info['limitType'] = limits[mg['LIMIT']] + if 'GAME' in mg: + (self.info['base'], self.info['category']) = games[mg['GAME']] + if 'SB' in mg: + self.info['sb'] = mg['SB'] + if 'BB' in mg: + self.info['bb'] = mg['BB'] + if mg['GAME'] == 'Holdem Tournament': + self.info['type'] = 'tour' + self.info['currency'] = 'T$' + else: + self.info['type'] = 'ring' + self.info['currency'] = 'USD' - # Override read function as xml.minidom barfs on the Carbon layout - # This is pretty dodgy - def readFile(self, filename): - print "Carbon: Reading file: '%s'" %(filename) - infile=open(filename, "rU") - self.obs = infile.read() - infile.close() - self.obs = "\n" + self.obs + "" - try: - doc = xml.dom.minidom.parseString(self.obs) - self.doc = doc - except: - traceback.print_exc(file=sys.stderr) + return self.info + + def readHandInfo(self, hand): + m = self.re_HandInfo.search(hand.handText) + if m is None: + logging.info("Didn't match re_HandInfo") + logging.info(hand.handText) + return None + logging.debug("HID %s-%s, Table %s" % (m.group('HID1'), + m.group('HID2'), m.group('TABLE')[:-1])) + hand.handid = m.group('HID1') + m.group('HID2') + hand.tablename = m.group('TABLE')[:-1] + hand.maxseats = 2 # This value may be increased as necessary + hand.starttime = datetime.datetime.strptime(m.group('DATETIME')[:12], + '%Y%m%d%H%M') + # Check that the hand is complete up to the awarding of the pot; if + # not, the hand is unparseable + if self.re_EndOfHand.search(hand.handText) is None: + raise FpdbParseError(hid=m.group('HID1') + "-" + m.group('HID2')) + + def readPlayerStacks(self, hand): + m = self.re_PlayerInfo.finditer(hand.handText) + for a in m: + seatno = int(a.group('SEAT')) + # It may be necessary to adjust 'hand.maxseats', which is an + # educated guess, starting with 2 (indicating a heads-up table) and + # adjusted upwards in steps to 6, then 9, then 10. An adjustment is + # made whenever a player is discovered whose seat number is + # currently above the maximum allowable for the table. + if seatno >= hand.maxseats: + if seatno > 8: + hand.maxseats = 10 + elif seatno > 5: + hand.maxseats = 9 + else: + hand.maxseats = 6 + if a.group('DEALTIN') == "true": + hand.addPlayer(seatno, a.group('PNAME'), a.group('CASH')) + + def markStreets(self, hand): + #if hand.gametype['base'] == 'hold': + m = re.search(r'(?P.+(?=(?P.+(?=(?P.+(?=(?P.+))?', hand.handText, re.DOTALL) + hand.addStreets(m) + + def readCommunityCards(self, hand, street): + m = self.re_Board.search(hand.streets[street]) + if street == 'FLOP': + hand.setCommunityCards(street, m.group('CARDS').split(',')) + elif street in ('TURN','RIVER'): + hand.setCommunityCards(street, [m.group('CARDS').split(',')[-1]]) + + def readAntes(self, hand): + pass # ??? + + def readBringIn(self, hand): + pass # ??? + + def readBlinds(self, hand): + try: + m = self.re_PostSB.search(hand.handText) + hand.addBlind(self.playerNameFromSeatNo(m.group('PSEAT'), hand), + 'small blind', m.group('SB')) + except: # no small blind + hand.addBlind(None, None, None) + for a in self.re_PostBB.finditer(hand.handText): + hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), hand), + 'big blind', a.group('BB')) + for a in self.re_PostBoth.finditer(hand.handText): + bb = Decimal(self.info['bb']) + amount = Decimal(a.group('SBBB')) + if amount < bb: + hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), + hand), 'small blind', a.group('SBBB')) + elif amount == bb: + hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), + hand), 'big blind', a.group('SBBB')) + else: + hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), + hand), 'both', a.group('SBBB')) + + def readButton(self, hand): + hand.buttonpos = int(self.re_Button.search(hand.handText).group('BUTTON')) + + def readHeroCards(self, hand): + m = self.re_HeroCards.search(hand.handText) + if m: + hand.hero = self.playerNameFromSeatNo(m.group('PSEAT'), hand) + cards = m.group('CARDS').split(',') + hand.addHoleCards('PREFLOP', hand.hero, closed=cards, shown=False, + mucked=False, dealt=True) + + def readAction(self, hand, street): + logging.debug("readAction (%s)" % street) + m = self.re_Action.finditer(hand.streets[street]) + for action in m: + logging.debug("%s %s" % (action.group('ATYPE'), + action.groupdict())) + player = self.playerNameFromSeatNo(action.group('PSEAT'), hand) + if action.group('ATYPE') == 'RAISE': + hand.addCallandRaise(street, player, action.group('BET')) + elif action.group('ATYPE') == 'CALL': + hand.addCall(street, player, action.group('BET')) + elif action.group('ATYPE') == 'BET': + hand.addBet(street, player, action.group('BET')) + elif action.group('ATYPE') in ('FOLD', 'SIT_OUT'): + hand.addFold(street, player) + elif action.group('ATYPE') == 'CHECK': + hand.addCheck(street, player) + elif action.group('ATYPE') == 'ALL_IN': + hand.addAllIn(street, player, action.group('BET')) + else: + logging.debug("Unimplemented readAction: %s %s" + % (action.group('PSEAT'),action.group('ATYPE'),)) + + def readShowdownActions(self, hand): + for shows in self.re_ShowdownAction.finditer(hand.handText): + cards = shows.group('CARDS').split(',') + hand.addShownCards(cards, + self.playerNameFromSeatNo(shows.group('PSEAT'), + hand)) + + def readCollectPot(self, hand): + pots = [Decimal(0) for n in range(hand.maxseats)] + for m in self.re_CollectPot.finditer(hand.handText): + pots[int(m.group('PSEAT'))] += Decimal(m.group('POT')) + # Regarding the processing logic for "committed", see Pot.end() in + # Hand.py + committed = sorted([(v,k) for (k,v) in hand.pot.committed.items()]) + for p in range(hand.maxseats): + pname = self.playerNameFromSeatNo(p, hand) + if committed[-1][1] == pname: + pots[p] -= committed[-1][0] - committed[-2][0] + if pots[p] > 0: + hand.addCollectPot(player=pname, pot=pots[p]) + + def readShownCards(self, hand): + for m in self.re_ShownCards.finditer(hand.handText): + cards = m.group('CARDS').split(',') + hand.addShownCards(cards=cards, player=self.playerNameFromSeatNo(m.group('PSEAT'), hand)) if __name__ == "__main__": - c = Configuration.Config() - e = CarbonPoker(c, "regression-test-files/carbon-poker/Niagara Falls (15245216).xml") - e.processFile() - print str(e) + parser = OptionParser() + parser.add_option("-i", "--input", dest="ipath", help="parse input hand history", default="-") + parser.add_option("-o", "--output", dest="opath", help="output translation to", default="-") + parser.add_option("-f", "--follow", dest="follow", help="follow (tail -f) the input", action="store_true", default=False) + parser.add_option("-q", "--quiet", action="store_const", const=logging.CRITICAL, dest="verbosity", default=logging.INFO) + parser.add_option("-v", "--verbose", action="store_const", const=logging.INFO, dest="verbosity") + parser.add_option("--vv", action="store_const", const=logging.DEBUG, dest="verbosity") + + (options, args) = parser.parse_args() + + LOG_FILENAME = './logging.out' + logging.basicConfig(filename=LOG_FILENAME, level=options.verbosity) + + e = Carbon(in_path = options.ipath, + out_path = options.opath, + follow = options.follow, + autostart = True) diff --git a/pyfpdb/Configuration.py b/pyfpdb/Configuration.py index abd5b016..9093c22f 100755 --- a/pyfpdb/Configuration.py +++ b/pyfpdb/Configuration.py @@ -56,36 +56,36 @@ def get_exec_path(): if hasattr(sys, "frozen"): # compiled by py2exe return os.path.dirname(sys.executable) else: - pathname = os.path.dirname(sys.argv[0]) - return os.path.abspath(pathname) + return sys.path[0] def get_config(file_name, fallback = True): - """Looks in cwd and in self.default_config_path for a config file.""" - config_path = os.path.join(get_exec_path(), file_name) -# print "config_path=", config_path - if os.path.exists(config_path): # there is a file in the cwd - return config_path # so we use it - else: # no file in the cwd, look where it should be in the first place - config_path = os.path.join(get_default_config_path(), file_name) -# print "config path 2=", config_path - if os.path.exists(config_path): + """Looks in exec dir and in self.default_config_path for a config file.""" + config_path = os.path.join(DIR_SELF, file_name) # look in exec dir + if os.path.exists(config_path) and os.path.isfile(config_path): + return config_path # there is a file in the exec dir so we use it + else: + config_path = os.path.join(DIR_CONFIG, file_name) # look in config dir + if os.path.exists(config_path) and os.path.isfile(config_path): return config_path # No file found if not fallback: return False -# OK, fall back to the .example file, should be in the start dir - if os.path.exists(file_name + ".example"): +# OK, fall back to the .example file, should be in the exec dir + if os.path.exists(os.path.join(DIR_SELF, file_name + ".example")): try: - shutil.copyfile(file_name + ".example", file_name) + shutil.copyfile(os.path.join(DIR_SELF, file_name + ".example"), os.path.join(DIR_CONFIG, file_name)) print "No %s found, using %s.example.\n" % (file_name, file_name) - print "A %s file has been created. You will probably have to edit it." % file_name - sys.stderr.write("No %s found, using %s.example.\n" % (file_name, file_name) ) + print "A %s file has been created. You will probably have to edit it." % os.path.join(DIR_CONFIG, file_name) + log.error("No %s found, using %s.example.\n" % (file_name, file_name) ) except: print "No %s found, cannot fall back. Exiting.\n" % file_name - sys.stderr.write("No %s found, cannot fall back. Exiting.\n" % file_name) sys.exit() + else: + print "No %s found, cannot fall back. Exiting.\n" % file_name + sys.stderr.write("No %s found, cannot fall back. Exiting.\n" % file_name) + sys.exit() return file_name def get_logger(file_name, config = "config", fallback = False): @@ -94,18 +94,26 @@ def get_logger(file_name, config = "config", fallback = False): try: logging.config.fileConfig(conf) log = logging.getLogger(config) - log.debug("%s logger initialised" % config) return log except: pass log = logging.basicConfig() log = logging.getLogger() - log.debug("config logger initialised") + log.error("basicConfig logger initialised") return log -# find a logging.conf file and set up logging -log = get_logger("logging.conf") +def check_dir(path, create = True): + """Check if a dir exists, optionally creates if not.""" + if os.path.exists(path): + if os.path.isdir(path): + return path + else: + return False + if create: + print "creating directory %s" % path + else: + return False ######################################################################## # application wide consts @@ -113,19 +121,31 @@ log = get_logger("logging.conf") APPLICATION_NAME_SHORT = 'fpdb' APPLICATION_VERSION = 'xx.xx.xx' -DIR_SELF = os.path.dirname(get_exec_path()) -#TODO: imo no good idea to place 'database' in parent dir -DIR_DATABASES = os.path.join(os.path.dirname(DIR_SELF), 'database') +DIR_SELF = get_exec_path() +DIR_CONFIG = check_dir(get_default_config_path()) +DIR_DATABASE = check_dir(os.path.join(DIR_CONFIG, 'database')) +DIR_LOG = check_dir(os.path.join(DIR_CONFIG, 'log')) DATABASE_TYPE_POSTGRESQL = 'postgresql' DATABASE_TYPE_SQLITE = 'sqlite' DATABASE_TYPE_MYSQL = 'mysql' +#TODO: should this be a tuple or a dict DATABASE_TYPES = ( DATABASE_TYPE_POSTGRESQL, DATABASE_TYPE_SQLITE, DATABASE_TYPE_MYSQL, ) +# find a logging.conf file and set up logging +log = get_logger("logging.conf", config = "config") +log.debug("config logger initialised") + +# and then log our consts +log.info("DIR SELF = %s" % DIR_SELF) +log.info("DIR CONFIG = %s" % DIR_CONFIG) +log.info("DIR DATABASE = %s" % DIR_DATABASE) +log.info("DIR LOG = %s" % DIR_LOG) +NEWIMPORT = True LOCALE_ENCODING = locale.getdefaultlocale()[1] ######################################################################## @@ -408,11 +428,10 @@ class Config: if file is not None: # config file path passed in file = os.path.expanduser(file) if not os.path.exists(file): - print "Configuration file %s not found. Using defaults." % (file) - sys.stderr.write("Configuration file %s not found. Using defaults." % (file)) + log.error("Specified configuration file %s not found. Using defaults." % (file)) file = None - if file is None: file = get_config("HUD_config.xml") + if file is None: file = get_config("HUD_config.xml", True) # Parse even if there was no real config file found and we are using the example # If using the example, we'll edit it later @@ -429,6 +448,8 @@ class Config: self.doc = doc self.file = file + self.dir = os.path.dirname(self.file) + self.dir_databases = os.path.join(self.dir, 'database') self.supported_sites = {} self.supported_games = {} self.supported_databases = {} # databaseName --> Database instance diff --git a/pyfpdb/Database.py b/pyfpdb/Database.py index bbbb27f1..d1d24e92 100644 --- a/pyfpdb/Database.py +++ b/pyfpdb/Database.py @@ -38,11 +38,31 @@ from decimal import Decimal import string import re import Queue +import codecs +import logging +import math + # pyGTK modules + +# Other library modules +try: + import sqlalchemy.pool as pool + use_pool = True +except ImportError: + logging.info("Not using sqlalchemy connection pool.") + use_pool = False + +try: + from numpy import var + use_numpy = True +except ImportError: + logging.info("Not using numpy to define variance in sqlite.") + use_numpy = False + + # FreePokerTools modules -import fpdb_db import Configuration import SQL import Card @@ -50,7 +70,29 @@ import Tourney import Charset from Exceptions import * -log = Configuration.get_logger("logging.conf") +log = Configuration.get_logger("logging.conf", config = "db") +log.debug("db logger initialized.") +encoder = codecs.lookup('utf-8') + +DB_VERSION = 119 + + +# Variance created as sqlite has a bunch of undefined aggregate functions. + +class VARIANCE: + def __init__(self): + self.store = [] + + def step(self, value): + self.store.append(value) + + def finalize(self): + return float(var(self.store)) + +class sqlitemath: + def mod(self, a, b): + return a%b + class Database: @@ -188,15 +230,14 @@ class Database: log.info("Creating Database instance, sql = %s" % sql) self.config = c self.__connected = False - self.fdb = fpdb_db.fpdb_db() # sets self.fdb.db self.fdb.cursor and self.fdb.sql - self.do_connect(c) - - if self.backend == self.PGSQL: - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE - #ISOLATION_LEVEL_AUTOCOMMIT = 0 - #ISOLATION_LEVEL_READ_COMMITTED = 1 - #ISOLATION_LEVEL_SERIALIZABLE = 2 - + self.settings = {} + self.settings['os'] = "linuxmac" if os.name != "nt" else "windows" + db_params = c.get_db_parameters() + self.import_options = c.get_import_parameters() + self.backend = db_params['db-backend'] + self.db_server = db_params['db-server'] + self.database = db_params['db-databaseName'] + self.host = db_params['db-host'] # where possible avoid creating new SQL instance by using the global one passed in if sql is None: @@ -204,6 +245,15 @@ class Database: else: self.sql = sql + # connect to db + self.do_connect(c) + print "connection =", self.connection + if self.backend == self.PGSQL: + from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE + #ISOLATION_LEVEL_AUTOCOMMIT = 0 + #ISOLATION_LEVEL_READ_COMMITTED = 1 + #ISOLATION_LEVEL_SERIALIZABLE = 2 + if self.backend == self.SQLITE and self.database == ':memory:' and self.wrongDbVersion: log.info("sqlite/:memory: - creating") self.recreate_tables() @@ -226,8 +276,6 @@ class Database: self.h_date_ndays_ago = 'd000000' # date N days ago ('d' + YYMMDD) for hero self.date_nhands_ago = {} # dates N hands ago per player - not used yet - self.cursor = self.fdb.cursor - self.saveActions = False if self.import_options['saveActions'] == False else True self.connection.rollback() # make sure any locks taken so far are released @@ -238,14 +286,20 @@ class Database: self.hud_style = style def do_connect(self, c): + if c is None: + raise FpdbError('Configuration not defined') + + db = c.get_db_parameters() try: - self.fdb.do_connect(c) + self.connect(backend=db['db-backend'], + host=db['db-host'], + database=db['db-databaseName'], + user=db['db-user'], + password=db['db-password']) except: # error during connect self.__connected = False raise - self.connection = self.fdb.db - self.wrongDbVersion = self.fdb.wrongDbVersion db_params = c.get_db_parameters() self.import_options = c.get_import_parameters() @@ -255,11 +309,137 @@ class Database: self.host = db_params['db-host'] self.__connected = True + def connect(self, backend=None, host=None, database=None, + user=None, password=None): + """Connects a database with the given parameters""" + if backend is None: + raise FpdbError('Database backend not defined') + self.backend = backend + self.host = host + self.user = user + self.password = password + self.database = database + self.connection = None + self.cursor = None + + if backend == Database.MYSQL_INNODB: + import MySQLdb + if use_pool: + MySQLdb = pool.manage(MySQLdb, pool_size=5) + try: + self.connection = MySQLdb.connect(host=host, user=user, passwd=password, db=database, use_unicode=True) + #TODO: Add port option + except MySQLdb.Error, ex: + if ex.args[0] == 1045: + raise FpdbMySQLAccessDenied(ex.args[0], ex.args[1]) + elif ex.args[0] == 2002 or ex.args[0] == 2003: # 2002 is no unix socket, 2003 is no tcp socket + raise FpdbMySQLNoDatabase(ex.args[0], ex.args[1]) + else: + print "*** WARNING UNKNOWN MYSQL ERROR", ex + elif backend == Database.PGSQL: + import psycopg2 + import psycopg2.extensions + if use_pool: + psycopg2 = pool.manage(psycopg2, pool_size=5) + psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) + # If DB connection is made over TCP, then the variables + # host, user and password are required + # For local domain-socket connections, only DB name is + # needed, and everything else is in fact undefined and/or + # flat out wrong + # sqlcoder: This database only connect failed in my windows setup?? + # Modifed it to try the 4 parameter style if the first connect fails - does this work everywhere? + connected = False + if self.host == "localhost" or self.host == "127.0.0.1": + try: + self.connection = psycopg2.connect(database = database) + connected = True + except: + # direct connection failed so try user/pass/... version + pass + if not connected: + try: + self.connection = psycopg2.connect(host = host, + user = user, + password = password, + database = database) + except Exception, ex: + if 'Connection refused' in ex.args[0]: + # meaning eg. db not running + raise FpdbPostgresqlNoDatabase(errmsg = ex.args[0]) + elif 'password authentication' in ex.args[0]: + raise FpdbPostgresqlAccessDenied(errmsg = ex.args[0]) + else: + msg = ex.args[0] + print msg + raise FpdbError(msg) + elif backend == Database.SQLITE: + logging.info("Connecting to SQLite: %(database)s" % {'database':database}) + import sqlite3 + if use_pool: + sqlite3 = pool.manage(sqlite3, pool_size=1) + else: + logging.warning("SQLite won't work well without 'sqlalchemy' installed.") + + if database != ":memory:": + if not os.path.isdir(self.config.dir_databases): + print "Creating directory: '%s'" % (self.config.dir_databases) + logging.info("Creating directory: '%s'" % (self.config.dir_databases)) + os.mkdir(self.config.dir_databases) + database = os.path.join(self.config.dir_databases, database) + logging.info(" sqlite db: " + database) + self.connection = sqlite3.connect(database, detect_types=sqlite3.PARSE_DECLTYPES ) + sqlite3.register_converter("bool", lambda x: bool(int(x))) + sqlite3.register_adapter(bool, lambda x: "1" if x else "0") + self.connection.create_function("floor", 1, math.floor) + tmp = sqlitemath() + self.connection.create_function("mod", 2, tmp.mod) + if use_numpy: + self.connection.create_aggregate("variance", 1, VARIANCE) + else: + logging.warning("Some database functions will not work without NumPy support") + else: + raise FpdbError("unrecognised database backend:"+backend) + + self.cursor = self.connection.cursor() + self.cursor.execute(self.sql.query['set tx level']) + self.check_version(database=database, create=True) + + + def check_version(self, database, create): + self.wrongDbVersion = False + try: + self.cursor.execute("SELECT * FROM Settings") + settings = self.cursor.fetchone() + if settings[0] != DB_VERSION: + logging.error("outdated or too new database version (%s) - please recreate tables" + % (settings[0])) + self.wrongDbVersion = True + except:# _mysql_exceptions.ProgrammingError: + if database != ":memory:": + if create: + print "Failed to read settings table - recreating tables" + log.info("failed to read settings table - recreating tables") + self.recreate_tables() + self.check_version(database=database, create=False) + if not self.wrongDbVersion: + msg = "Edit your screen_name and hand history path in the supported_sites "\ + +"section of the \nPreferences window (Main menu) before trying to import hands" + print "\n%s" % msg + log.warning(msg) + else: + print "Failed to read settings table - please recreate tables" + log.info("failed to read settings table - please recreate tables") + self.wrongDbVersion = True + else: + self.wrongDbVersion = True + #end def connect + def commit(self): - self.fdb.db.commit() + self.connection.commit() def rollback(self): - self.fdb.db.rollback() + self.connection.rollback() def connected(self): return self.__connected @@ -272,11 +452,18 @@ class Database: def disconnect(self, due_to_error=False): """Disconnects the DB (rolls back if param is true, otherwise commits""" - self.fdb.disconnect(due_to_error) + if due_to_error: + self.connection.rollback() + else: + self.connection.commit() + self.cursor.close() + self.connection.close() def reconnect(self, due_to_error=False): """Reconnects the DB""" - self.fdb.reconnect(due_to_error=False) + #print "started reconnect" + self.disconnect(due_to_error) + self.connect(self.backend, self.host, self.database, self.user, self.password) def get_backend_name(self): """Returns the name of the currently used backend""" @@ -289,6 +476,9 @@ class Database: else: raise FpdbError("invalid backend") + def get_db_info(self): + return (self.host, self.database, self.user, self.password) + def get_table_name(self, hand_id): c = self.connection.cursor() c.execute(self.sql.query['get_table_name'], (hand_id, )) @@ -845,6 +1035,7 @@ class Database: self.create_tables() self.createAllIndexes() self.commit() + print "Finished recreating tables" log.info("Finished recreating tables") #end def recreate_tables @@ -1110,7 +1301,7 @@ class Database: def fillDefaultData(self): c = self.get_cursor() - c.execute("INSERT INTO Settings (version) VALUES (118);") + c.execute("INSERT INTO Settings (version) VALUES (%s);" % (DB_VERSION)) c.execute("INSERT INTO Sites (name,currency) VALUES ('Full Tilt Poker', 'USD')") c.execute("INSERT INTO Sites (name,currency) VALUES ('PokerStars', 'USD')") c.execute("INSERT INTO Sites (name,currency) VALUES ('Everleaf', 'USD')") @@ -1121,6 +1312,7 @@ class Database: c.execute("INSERT INTO Sites (name,currency) VALUES ('Absolute', 'USD')") c.execute("INSERT INTO Sites (name,currency) VALUES ('PartyPoker', 'USD')") c.execute("INSERT INTO Sites (name,currency) VALUES ('Partouche', 'EUR')") + c.execute("INSERT INTO Sites (name,currency) VALUES ('Carbon', 'USD')") if self.backend == self.SQLITE: c.execute("INSERT INTO TourneyTypes (id, siteId, buyin, fee) VALUES (NULL, 1, 0, 0);") elif self.backend == self.PGSQL: @@ -1266,7 +1458,7 @@ class Database: try: self.get_cursor().execute(self.sql.query['lockForInsert']) except: - print "Error during fdb.lock_for_insert:", str(sys.exc_value) + print "Error during lock_for_insert:", str(sys.exc_value) #end def lock_for_insert ########################### @@ -1285,6 +1477,7 @@ class Database: p['tableName'], p['gameTypeId'], p['siteHandNo'], + 0, # tourneyId: 0 means not a tourney hand p['handStart'], datetime.today(), #importtime p['seats'], diff --git a/pyfpdb/Filters.py b/pyfpdb/Filters.py index 37a31c9e..fa6d2400 100644 --- a/pyfpdb/Filters.py +++ b/pyfpdb/Filters.py @@ -27,8 +27,8 @@ import gobject #import pokereval import Configuration -import fpdb_db -import FpdbSQLQueries +import Database +import SQL import Charset class Filters(threading.Thread): @@ -800,10 +800,10 @@ def main(argv=None): config = Configuration.Config() db = None - db = fpdb_db.fpdb_db() + db = Database.Database() db.do_connect(config) - qdict = FpdbSQLQueries.FpdbSQLQueries(db.get_backend_name()) + qdict = SQL.SQL(db.get_backend_name()) i = Filters(db, config, qdict) main_window = gtk.Window() diff --git a/pyfpdb/FulltiltToFpdb.py b/pyfpdb/FulltiltToFpdb.py index 19f834e6..2ee8bdd5 100755 --- a/pyfpdb/FulltiltToFpdb.py +++ b/pyfpdb/FulltiltToFpdb.py @@ -18,12 +18,10 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ######################################################################## -import sys import logging from HandHistoryConverter import * # Fulltilt HH Format converter -# TODO: cat tourno and table to make table name for tournaments class Fulltilt(HandHistoryConverter): @@ -67,8 +65,8 @@ class Fulltilt(HandHistoryConverter): (\s\((?PTurbo)\))?)|(?P.+)) ''', re.VERBOSE) re_Button = re.compile('^The button is in seat #(?P