Coverage for HandHistoryConverter.py: 20%
459 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-28 16:41 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-28 16:41 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
4#Copyright 2008-2011 Carl Gherardi
5#This program is free software: you can redistribute it and/or modify
6#it under the terms of the GNU Affero General Public License as published by
7#the Free Software Foundation, version 3 of the License.
8#
9#This program is distributed in the hope that it will be useful,
10#but WITHOUT ANY WARRANTY; without even the implied warranty of
11#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12#GNU General Public License for more details.
13#
14#You should have received a copy of the GNU Affero General Public License
15#along with this program. If not, see <http://www.gnu.org/licenses/>.
16#In the "official" distribution you can find the license in agpl-3.0.txt.
17#
18from __future__ import print_function
19from __future__ import division
22from past.utils import old_div
23#import L10n
24#_ = L10n.get_translation()
26import re
27import sys
28import traceback
29from optparse import OptionParser
30import os
31import os.path
32import xml.dom.minidom
33import codecs
34from decimal_wrapper import Decimal
35import operator
36from xml.dom.minidom import Node
38import time
39import datetime
41from pytz import timezone
42import pytz
44import logging
45# logging has been set up in fpdb.py or HUD_main.py, use their settings:
46log = logging.getLogger("parser")
49import Hand
50from Exceptions import *
51import Configuration
53class HandHistoryConverter(object):
55 READ_CHUNK_SIZE = 10000 # bytes to read at a time from file in tail mode
57 # filetype can be "text" or "xml"
58 # so far always "text"
59 # subclass HHC_xml for xml parsing
60 filetype = "text"
62 # codepage indicates the encoding of the text file.
63 # cp1252 is a safe default
64 # "utf_8" is more likely if there are funny characters
65 codepage = "cp1252"
67 re_tzOffset = re.compile('^\w+[+-]\d{4}$')
68 copyGameHeader = False
69 summaryInFile = False
71 # maybe archive params should be one archive param, then call method in specific converter. if archive: convert_archive()
72 def __init__( self, config, in_path = '-', out_path = '-', index=0
73 , autostart=True, starsArchive=False, ftpArchive=False, sitename="PokerStars"):
74 """\
75in_path (default '-' = sys.stdin)
76out_path (default '-' = sys.stdout)
77"""
79 self.config = config
80 self.import_parameters = self.config.get_import_parameters()
81 self.sitename = sitename
82 log.info("HandHistory init - %s site, %s subclass, in_path '%r'; out_path '%r'"
83 % (self.sitename, self.__class__, in_path, out_path) ) # should use self.filter, not self.sitename
85 self.index = index
86 self.starsArchive = starsArchive
87 self.ftpArchive = ftpArchive
89 self.in_path = in_path
90 self.base_name = self.getBasename()
91 self.out_path = out_path
92 self.kodec = None
94 self.processedHands = []
95 self.numHands = 0
96 self.numErrors = 0
97 self.numPartial = 0
98 self.isCarraige = False
99 self.autoPop = False
101 # Tourney object used to store TourneyInfo when called to deal with a Summary file
102 self.tourney = None
104 if in_path == '-':
105 self.in_fh = sys.stdin
106 self.out_fh = get_out_fh(out_path, self.import_parameters)
108 self.compiledPlayers = set()
109 self.maxseats = 0
111 self.status = True
113 self.parsedObjectType = "HH" #default behaviour : parsing HH files, can be "Summary" if the parsing encounters a Summary File
116 if autostart:
117 self.start()
119 def __str__(self):
120 return """
121HandHistoryConverter: '%(sitename)s'
122 filetype '%(filetype)s'
123 in_path '%(in_path)s'
124 out_path '%(out_path)s'
125 """ % locals()
127 def start(self):
128 """Process a hand at a time from the input specified by in_path."""
129 starttime = time.time()
130 if not self.sanityCheck():
131 log.warning(("Failed sanity check"))
132 return
134 self.numHands = 0
135 self.numPartial = 0
136 self.numSkipped = 0
137 self.numErrors = 0
138 lastParsed = None
139 handsList = self.allHandsAsList()
140 log.debug( ("Hands list is:") + str(handsList))
141 log.info(("Parsing %d hands") % len(list(handsList)))
142 # Determine if we're dealing with a HH file or a Summary file
143 # quick fix : empty files make the handsList[0] fail ==> If empty file, go on with HH parsing
144 if len(list(handsList)) == 0 or self.isSummary(handsList[0]) == False:
145 self.parsedObjectType = "HH"
146 for handText in handsList:
147 try:
148 self.processedHands.append(self.processHand(handText))
149 lastParsed = 'stored'
150 except FpdbHandPartial as e:
151 self.numPartial += 1
152 lastParsed = 'partial'
153 log.debug("%s" % e)
154 except FpdbHandSkipped as e:
155 self.numSkipped += 1
156 lastParsed = 'skipped'
157 except FpdbParseError:
158 self.numErrors += 1
159 lastParsed = 'error'
160 log.error(("FpdbParseError for file '%s'") % self.in_path)
161 if lastParsed in ('partial', 'error') and self.autoPop:
162 self.index -= len(handsList[-1])
163 if self.isCarraige:
164 self.index -= handsList[-1].count('\n')
165 handsList.pop()
166 if lastParsed=='partial':
167 self.numPartial -= 1
168 else:
169 self.numErrors -= 1
170 log.info(("Removing partially written hand & resetting index"))
171 self.numHands = len(list(handsList))
172 endtime = time.time()
173 log.info(("Read %d hands (%d failed) in %.3f seconds") % (self.numHands, (self.numErrors + self.numPartial), endtime - starttime))
174 else:
175 self.parsedObjectType = "Summary"
176 summaryParsingStatus = self.readSummaryInfo(handsList)
177 endtime = time.time()
178 if summaryParsingStatus :
179 log.info(("Summary file '%s' correctly parsed (took %.3f seconds)") % (self.in_path, endtime - starttime))
180 else :
181 log.warning(("Error converting summary file '%s' (took %.3f seconds)") % (self.in_path, endtime - starttime))
183 def setAutoPop(self, value):
184 self.autoPop = value
186 def allHandsAsList(self):
187 """Return a list of handtexts in the file at self.in_path"""
188 #TODO : any need for this to be generator? e.g. stars support can email one huge file of all hands in a year. Better to read bit by bit than all at once.
189 self.readFile()
190 lenobs = len(self.obs)
191 self.obs = self.obs.rstrip()
192 self.index -= (lenobs - len(self.obs))
193 self.obs = self.obs.lstrip()
194 lenobs = len(self.obs)
195 self.obs = self.obs.replace('\r\n', '\n').replace(u'\xa0', u' ')
196 if lenobs != len(self.obs):
197 self.isCarraige = True
198 # maybe archive params should be one archive param, then call method in specific converter?
199 # if self.archive:
200 # self.obs = self.convert_archive(self.obs)
201 if self.starsArchive == True:
202 m = re.compile('^Hand #\d+', re.MULTILINE)
203 self.obs = m.sub('', self.obs)
205 if self.ftpArchive == True:
206 # Remove ******************** # 1 *************************
207 m = re.compile('\*{20}\s#\s\d+\s\*{20,25}\s+', re.MULTILINE)
208 self.obs = m.sub('', self.obs)
210 if self.obs is None or self.obs == "":
211 log.info(("Read no hands from file: '%s'") % self.in_path)
212 return []
213 handlist = re.split(self.re_SplitHands, self.obs)
214 # Some HH formats leave dangling text after the split
215 # ie. </game> (split) </session>EOL
216 # Remove this dangler if less than 50 characters and warn in the log
217 if len(handlist[-1]) <= 50:
218 self.index -= len(handlist[-1])
219 if self.isCarraige:
220 self.index -= handlist[-1].count('\n')
221 handlist.pop()
222 log.info(("Removing text < 50 characters & resetting index"))
223 return handlist
225 def processHand(self, handText):
226 if self.isPartial(handText):
227 raise FpdbHandPartial(("Could not identify as a %s hand") % self.sitename)
228 if self.copyGameHeader:
229 gametype = self.parseHeader(handText, self.whole_file.replace('\r\n', '\n').replace(u'\xa0', u' '))
230 else:
231 gametype = self.determineGameType(handText)
232 hand = None
233 l = None
234 if gametype is None:
235 gametype = "unmatched"
236 # TODO: not ideal, just trying to not error. Throw ParseException?
237 self.numErrors += 1
238 else:
239 print(gametype)
240 print('gametypecategory',gametype['category'])
241 if gametype['category'] in self.import_parameters['importFilters']:
242 raise FpdbHandSkipped("Skipped %s hand" % gametype['type'])
243 # See if gametype is supported.
244 if 'mix' not in gametype: gametype['mix'] = 'none'
245 if 'ante' not in gametype: gametype['ante'] = 0
246 if 'buyinType' not in gametype: gametype['buyinType'] = 'regular'
247 if 'fast' not in gametype: gametype['fast'] = False
248 if 'newToGame' not in gametype: gametype['newToGame'] = False
249 if 'homeGame' not in gametype: gametype['homeGame'] = False
250 if 'split' not in gametype: gametype['split'] = False
251 type = gametype['type']
252 base = gametype['base']
253 limit = gametype['limitType']
254 l = [type] + [base] + [limit]
256 if l in self.readSupportedGames():
257 if gametype['base'] == 'hold':
258 hand = Hand.HoldemOmahaHand(self.config, self, self.sitename, gametype, handText)
259 elif gametype['base'] == 'stud':
260 hand = Hand.StudHand(self.config, self, self.sitename, gametype, handText)
261 elif gametype['base'] == 'draw':
262 hand = Hand.DrawHand(self.config, self, self.sitename, gametype, handText)
263 else:
264 log.error(("%s Unsupported game type: %s") % (self.sitename, gametype))
265 raise FpdbParseError
267 if hand:
268 #hand.writeHand(self.out_fh)
269 return hand
270 else:
271 log.error(("%s Unsupported game type: %s") % (self.sitename, gametype))
272 # TODO: pity we don't know the HID at this stage. Log the entire hand?
274 def isPartial(self, handText):
275 count = 0
276 for m in self.re_Identify.finditer(handText):
277 count += 1
278 if count!=1:
279 return True
280 return False
282 # These functions are parse actions that may be overridden by the inheriting class
283 # This function should return a list of lists looking like:
284 # return [["ring", "hold", "nl"], ["tour", "hold", "nl"]]
285 # Showing all supported games limits and types
287 def readSupportedGames(self): abstract
289 # should return a list
290 # type base limit
291 # [ ring, hold, nl , sb, bb ]
292 # Valid types specified in docs/tabledesign.html in Gametypes
293 def determineGameType(self, handText): abstract
294 """return dict with keys/values:
295 'type' in ('ring', 'tour')
296 'limitType' in ('nl', 'cn', 'pl', 'cp', 'fl')
297 'base' in ('hold', 'stud', 'draw')
298 'category' in ('holdem', 'omahahi', omahahilo', 'fusion', 'razz', 'studhi', 'studhilo', 'fivedraw', '27_1draw', '27_3draw', 'badugi')
299 'hilo' in ('h','l','s')
300 'mix' in (site specific, or 'none')
301 'smallBlind' int?
302 'bigBlind' int?
303 'smallBet'
304 'bigBet'
305 'currency' in ('USD', 'EUR', 'T$', <countrycode>)
306or None if we fail to get the info """
307 #TODO: which parts are optional/required?
309 def readHandInfo(self, hand): abstract
310 """Read and set information about the hand being dealt, and set the correct
311 variables in the Hand object 'hand
313 * hand.startTime - a datetime object
314 * hand.handid - The site identified for the hand - a string.
315 * hand.tablename
316 * hand.buttonpos
317 * hand.maxseats
318 * hand.mixed
320 Tournament fields:
322 * hand.tourNo - The site identified tournament id as appropriate - a string.
323 * hand.buyin
324 * hand.fee
325 * hand.buyinCurrency
326 * hand.koBounty
327 * hand.isKO
328 * hand.level
329 """
330 #TODO: which parts are optional/required?
332 def readPlayerStacks(self, hand): abstract
333 """This function is for identifying players at the table, and to pass the
334 information on to 'hand' via Hand.addPlayer(seat, name, chips)
336 At the time of writing the reference function in the PS converter is:
337 log.debug("readPlayerStacks")
338 m = self.re_PlayerInfo.finditer(hand.handText)
339 for a in m:
340 hand.addPlayer(int(a.group('SEAT')), a.group('PNAME'), a.group('CASH'))
342 Which is pretty simple because the hand history format is consistent. Other hh formats aren't so nice.
344 This is the appropriate place to identify players that are sitting out and ignore them
346 *** NOTE: You may find this is a more appropriate place to set hand.maxseats ***
347 """
349 def compilePlayerRegexs(self): abstract
350 """Compile dynamic regexes -- compile player dependent regexes.
352 Depending on the ambiguity of lines you may need to match, and the complexity of
353 player names - we found that we needed to recompile some regexes for player actions so that they actually contained the player names.
355 eg.
356 We need to match the ante line:
357 <Player> antes $1.00
359 But <Player> is actually named
361 YesI antes $4000 - A perfectly legal playername
363 Giving:
365 YesI antes $4000 antes $1.00
367 Which without care in your regexes most people would match 'YesI' and not 'YesI antes $4000'
368 """
370 # Needs to return a MatchObject with group names identifying the streets into the Hand object
371 # so groups are called by street names 'PREFLOP', 'FLOP', 'STREET2' etc
372 # blinds are done seperately
373 def markStreets(self, hand): abstract
374 """For dividing the handText into sections.
376 The function requires you to pass a MatchObject with groups specifically labeled with
377 the 'correct' street names.
379 The Hand object will use the various matches for assigning actions to the correct streets.
381 Flop Based Games:
382 PREFLOP, FLOP, TURN, RIVER
384 Draw Based Games:
385 PREDEAL, DEAL, DRAWONE, DRAWTWO, DRAWTHREE
387 Stud Based Games:
388 ANTES, THIRD, FOURTH, FIFTH, SIXTH, SEVENTH
390 The Stars HHC has a good reference implementation
391 """
393 #Needs to return a list in the format
394 # ['player1name', 'player2name', ...] where player1name is the sb and player2name is bb,
395 # addtional players are assumed to post a bb oop
396 def readBlinds(self, hand): abstract
397 """Function for reading the various blinds from the hand history.
399 Pass any small blind to hand.addBlind(<name>, "small blind", <value>)
400 - unless it is a single dead small blind then use:
401 hand.addBlind(<name>, 'secondsb', <value>)
402 Pass any big blind to hand.addBlind(<name>, "big blind", <value>)
403 Pass any play posting both big and small blinds to hand.addBlind(<name>, 'both', <vale>)
404 """
405 def readSTP(self, hand): pass
406 def readAntes(self, hand): abstract
407 """Function for reading the antes from the hand history and passing the hand.addAnte"""
408 def readBringIn(self, hand): abstract
409 def readButton(self, hand): abstract
410 def readHoleCards(self, hand): abstract
411 def readAction(self, hand, street): abstract
412 def readCollectPot(self, hand): abstract
413 def readShownCards(self, hand): abstract
414 def readTourneyResults(self, hand):
415 """This function is for future use in parsing tourney results directly from a hand"""
416 pass
418 # EDIT: readOther is depreciated
419 # Some sites do odd stuff that doesn't fall in to the normal HH parsing.
420 # e.g., FTP doesn't put mixed game info in the HH, but puts in in the
421 # file name. Use readOther() to clean up those messes.
422 def readOther(self, hand): pass
424 # Some sites don't report the rake. This will be called at the end of the hand after the pot total has been calculated
425 # an inheriting class can calculate it for the specific site if need be.
426 def getRake(self, hand):
427 print('total pot', hand.totalpot)
428 print('collected pot', hand.totalcollected)
429 if hand.totalcollected>hand.totalpot:
430 print("collected pot>total pot")
431 if hand.rake is None:
432 hand.rake = hand.totalpot - hand.totalcollected # * Decimal('0.05') # probably not quite right
433 if self.siteId == 9 and hand.gametype['type'] == "tour":
434 round = -5 #round up to 10
435 elif hand.gametype['type'] == "tour":
436 round = -1
437 else:
438 round = -0.01
439 if self.siteId == 15 and hand.totalcollected>hand.totalpot:
440 hand.rake = old_div(hand.totalpot, 10)
441 print(hand.rake)
442 if hand.rake < 0 and (not hand.roundPenny or hand.rake < round) and not hand.cashedOut:
443 if (self.siteId == 28 and
444 ((hand.rake + Decimal(str(hand.sb)) - (0 if hand.rakes.get('rake') is None else hand.rakes['rake'])) == 0 or
445 (hand.rake + Decimal(str(hand.sb)) + Decimal(str(hand.bb)) - (0 if hand.rakes.get('rake') is None else hand.rakes['rake'])) == 0)
446 ):
447 log.error(("hhc.getRake(): '%s': Missed sb/bb - Amount collected (%s) is greater than the pot (%s)") % (hand.handid,str(hand.totalcollected), str(hand.totalpot)))
448 else:
449 log.error(("hhc.getRake(): '%s': Amount collected (%s) is greater than the pot (%s)") % (hand.handid,str(hand.totalcollected), str(hand.totalpot)))
450 raise FpdbParseError
451 elif hand.totalpot > 0 and Decimal(old_div(hand.totalpot,4)) < hand.rake and not hand.fastFold and not hand.cashedOut:
452 log.error(("hhc.getRake(): '%s': Suspiciously high rake (%s) > 25 pct of pot (%s)") % (hand.handid,str(hand.rake), str(hand.totalpot)))
453 raise FpdbParseError
455 def sanityCheck(self):
456 """Check we aren't going to do some stupid things"""
457 sane = False
458 base_w = False
460 # Make sure input and output files are different or we'll overwrite the source file
461 if True: # basically.. I don't know
462 sane = True
464 if self.in_path != '-' and self.out_path == self.in_path:
465 print(("Output and input files are the same, check config."))
466 sane = False
468 return sane
470 # Functions not necessary to implement in sub class
471 def setFileType(self, filetype = "text", codepage='utf8'):
472 self.filetype = filetype
473 self.codepage = codepage
475 # Import from string
476 def setObs(self, text):
477 self.obs = text
478 self.whole_file = text
480 def __listof(self, x):
481 if isinstance(x, list) or isinstance(x, tuple):
482 return x
483 else:
484 return [x]
486 def readFile(self):
487 """Open in_path according to self.codepage. Exceptions caught further up"""
489 if self.filetype == "text":
490 for kodec in self.__listof(self.codepage):
491 #print "trying", kodec
492 try:
493 in_fh = codecs.open(self.in_path, 'r', kodec)
494 self.whole_file = in_fh.read()
495 in_fh.close()
496 self.obs = self.whole_file[self.index:]
497 self.index = len(self.whole_file)
498 self.kodec = kodec
499 return True
500 except:
501 pass
502 else:
503 log.error(("unable to read file with any codec in list!") + " " + self.in_path)
504 self.obs = ""
505 return False
506 elif self.filetype == "xml":
507 doc = xml.dom.minidom.parse(filename)
508 self.doc = doc
509 elif self.filetype == "":
510 pass
512 def guessMaxSeats(self, hand):
513 """Return a guess at maxseats when not specified in HH."""
514 # if some other code prior to this has already set it, return it
515 if not self.copyGameHeader and hand.gametype['type']=='tour':
516 return 10
518 if self.maxseats > 1 and self.maxseats < 11:
519 return self.maxseats
521 mo = self.maxOccSeat(hand)
523 if mo == 10: return 10 #that was easy
525 if hand.gametype['base'] == 'stud':
526 if mo <= 8: return 8
528 if hand.gametype['base'] == 'draw':
529 if mo <= 6: return 6
531 return 10
533 def maxOccSeat(self, hand):
534 max = 0
535 for player in hand.players:
536 if int(player[0]) > max:
537 max = int(player[0])
538 return max
540 def getStatus(self):
541 #TODO: Return a status of true if file processed ok
542 return self.status
544 def getProcessedHands(self):
545 return self.processedHands
547 def getProcessedFile(self):
548 return self.out_path
550 def getLastCharacterRead(self):
551 return self.index
553 def isSummary(self, topline):
554 return " Tournament Summary " in topline
556 def getParsedObjectType(self):
557 return self.parsedObjectType
559 def getBasename(self):
560 head, tail = os.path.split(self.in_path)
561 base = tail or os.path.basename(head)
562 return base.split('.')[0]
564 #returns a status (True/False) indicating wether the parsing could be done correctly or not
565 def readSummaryInfo(self, summaryInfoList): abstract
567 def getTourney(self):
568 return self.tourney
570 @staticmethod
571 def changeTimezone(time, givenTimezone, wantedTimezone):
572 """Takes a givenTimezone in format AAA or AAA+HHMM where AAA is a standard timezone
573 and +HHMM is an optional offset (+/-) in hours (HH) and minutes (MM)
574 (See OnGameToFpdb.py for example use of the +HHMM part)
575 Tries to convert the time parameter (with no timezone) from the givenTimezone to
576 the wantedTimeZone (currently only allows "UTC")
577 """
578 #log.debug("raw time: " + str(time) + " given time zone: " + str(givenTimezone))
579 if wantedTimezone=="UTC":
580 wantedTimezone = pytz.utc
581 else:
582 log.error(("Unsupported target timezone: ") + givenTimezone)
583 raise FpdbParseError(("Unsupported target timezone: ") + givenTimezone)
585 givenTZ = None
586 if HandHistoryConverter.re_tzOffset.match(givenTimezone):
587 offset = int(givenTimezone[-5:])
588 givenTimezone = givenTimezone[0:-5]
589 #log.debug("changeTimeZone: offset=" + str(offset))
590 else: offset=0
592 if givenTimezone in ("ET", "EST", "EDT"):
593 givenTZ = timezone('US/Eastern')
594 elif givenTimezone in ("CET", "CEST", "MEZ", "MESZ", "HAEC"):
595 #since CEST will only be used in summer time it's ok to treat it as identical to CET.
596 givenTZ = timezone('Europe/Berlin')
597 #Note: Daylight Saving Time is standardised across the EU so this should be fine
598 elif givenTimezone in ('GT', 'GMT'): # GMT is always the same as UTC
599 givenTZ = timezone('GMT')
600 # GMT cannot be treated as WET because some HH's are explicitly
601 # GMT+-delta so would be incorrect during the summertime
602 # if substituted as WET+-delta
603 elif givenTimezone == 'BST':
604 givenTZ = timezone('Europe/London')
605 elif givenTimezone == 'WET': # WET is GMT with daylight saving delta
606 givenTZ = timezone('WET')
607 elif givenTimezone in ('HT', 'HST', 'HDT'): # Hawaiian Standard Time
608 givenTZ = timezone('US/Hawaii')
609 elif givenTimezone == 'AKT': # Alaska Time
610 givenTZ = timezone('US/Alaska')
611 elif givenTimezone in ('PT', 'PST', 'PDT'): # Pacific Time
612 givenTZ = timezone('US/Pacific')
613 elif givenTimezone in ('MT', 'MST', 'MDT'): # Mountain Time
614 givenTZ = timezone('US/Mountain')
615 elif givenTimezone in ('CT', 'CST', 'CDT'): # Central Time
616 givenTZ = timezone('US/Central')
617 elif givenTimezone == 'AT': # Atlantic Time
618 givenTZ = timezone('Canada/Atlantic')
619 elif givenTimezone == 'NT': # Newfoundland Time
620 givenTZ = timezone('Canada/Newfoundland')
621 elif givenTimezone == 'ART': # Argentinian Time
622 givenTZ = timezone('America/Argentina/Buenos_Aires')
623 elif givenTimezone in ('BRT', 'BRST'): # Brasilia Time
624 givenTZ = timezone('America/Sao_Paulo')
625 elif givenTimezone == 'VET':
626 givenTZ = timezone('America/Caracas')
627 elif givenTimezone == 'COT':
628 givenTZ = timezone('America/Bogota')
629 elif givenTimezone in ('EET', 'EEST'): # Eastern European Time
630 givenTZ = timezone('Europe/Bucharest')
631 elif givenTimezone in ('MSK', 'MESZ', 'MSKS', 'MSD'): # Moscow Standard Time
632 givenTZ = timezone('Europe/Moscow')
633 elif givenTimezone == 'GST':
634 givenTZ = timezone('Asia/Dubai')
635 elif givenTimezone in ('YEKT','YEKST'):
636 givenTZ = timezone('Asia/Yekaterinburg')
637 elif givenTimezone in ('KRAT','KRAST'):
638 givenTZ = timezone('Asia/Krasnoyarsk')
639 elif givenTimezone == 'IST': # India Standard Time
640 givenTZ = timezone('Asia/Kolkata')
641 elif givenTimezone == 'ICT':
642 givenTZ = timezone('Asia/Bangkok')
643 elif givenTimezone == 'CCT': # China Coast Time
644 givenTZ = timezone('Australia/West')
645 elif givenTimezone == 'JST': # Japan Standard Time
646 givenTZ = timezone('Asia/Tokyo')
647 elif givenTimezone in ('AWST', 'AWT'): # Australian Western Standard Time
648 givenTZ = timezone('Australia/West')
649 elif givenTimezone in ('ACST', 'ACT'): # Australian Central Standard Time
650 givenTZ = timezone('Australia/Darwin')
651 elif givenTimezone in ('AEST', 'AET'): # Australian Eastern Standard Time
652 # Each State on the East Coast has different DSTs.
653 # Melbournce is out because I don't like AFL, Queensland doesn't have DST
654 # ACT is full of politicians and Tasmania will never notice.
655 # Using Sydney.
656 givenTZ = timezone('Australia/Sydney')
657 elif givenTimezone in ('NZST', 'NZT', 'NZDT'): # New Zealand Time
658 givenTZ = timezone('Pacific/Auckland')
659 elif givenTimezone == 'UTC': # Universal time co-ordinated
660 givenTZ = pytz.UTC
661 elif givenTimezone in pytz.all_timezones:
662 givenTZ = timezone(givenTimezone)
663 else:
664 timezone_lookup = dict([(pytz.timezone(x).localize(datetime.datetime.now()).tzname(), x) for x in pytz.all_timezones])
665 if givenTimezone in timezone_lookup:
666 givenTZ = timezone(timezone_lookup[givenTimezone])
668 if givenTZ is None:
669 # do not crash if timezone not in list, just return UTC localized time
670 log.error(("Timezone conversion not supported") + ": " + givenTimezone + " " + str(time))
671 givenTZ = pytz.UTC
672 return givenTZ.localize(time)
674 localisedTime = givenTZ.localize(time)
675 utcTime = localisedTime.astimezone(wantedTimezone) + datetime.timedelta(seconds=-3600*(old_div(offset,100))-60*(offset%100))
676 #log.debug("utcTime: " + str(utcTime))
677 return utcTime
678 #end @staticmethod def changeTimezone
680 @staticmethod
681 def getTableTitleRe(type, table_name=None, tournament = None, table_number=None):
682 "Returns string to search in windows titles"
683 if type=="tour":
684 return ( re.escape(str(tournament)) + ".+\\Table " + re.escape(str(table_number)) )
685 else:
686 return re.escape(table_name)
688 @staticmethod
689 def getTableNoRe(tournament):
690 "Returns string to search window title for tournament table no."
691# Full Tilt: $30 + $3 Tournament (181398949), Table 1 - 600/1200 Ante 100 - Limit Razz
692# PokerStars: WCOOP 2nd Chance 02: $1,050 NLHE - Tournament 307521826 Table 1 - Blinds $30/$60
693 return "%s.+(?:Table|Torneo) (\d+)" % (tournament, )
695 @staticmethod
696 def clearMoneyString(money):
697 """Converts human readable string representations of numbers like
698 '1 200', '2,000', '0,01' to more machine processable form - no commas, 1 decimal point
699 """
700 if not money:
701 return money
702 money = money.replace(' ', '')
703 money = money.replace(u'\xa0', u'')
704 if 'K' in money:
705 money = money.replace('K', '000')
706 if 'M' in money:
707 money = money.replace('M', '000000')
708 if 'B' in money:
709 money = money.replace('B', '000000000')
710 if money[-1] in ('.', ','):
711 money = money[:-1]
712 if len(money) < 3:
713 return money # No commas until 0,01 or 1,00
714 if money[-3] == ',':
715 money = money[:-3] + '.' + money[-2:]
716 if len(money) > 15:
717 if money[-15] == '.':
718 money = money[:-15] + ',' + money[-14:]
719 if len(money) > 11:
720 if money[-11] == '.':
721 money = money[:-11] + ',' + money[-10:]
722 if len(money) > 7:
723 if money[-7] == '.':
724 money = money[:-7] + ',' + money[-6:]
725 else:
726 if len(money) > 12:
727 if money[-12] == '.':
728 money = money[:-12] + ',' + money[-11:]
729 if len(money) > 8:
730 if money[-8] == '.':
731 money = money[:-8] + ',' + money[-7:]
732 if len(money) > 4:
733 if money[-4] == '.':
734 money = money[:-4] + ',' + money[-3:]
736 return money.replace(',', '').replace("'", '')
739def getTableTitleRe(config, sitename, *args, **kwargs):
740 "Returns string to search in windows titles for current site"
741 return getSiteHhc(config, sitename).getTableTitleRe(*args, **kwargs)
743def getTableNoRe(config, sitename, *args, **kwargs):
744 "Returns string to search window titles for tournament table no."
745 return getSiteHhc(config, sitename).getTableNoRe(*args, **kwargs)
749def getSiteHhc(config, sitename):
750 "Returns HHC class for current site"
751 hhcName = config.hhcs[sitename].converter
752 hhcModule = __import__(hhcName)
753 return getattr(hhcModule, hhcName[:-6])
755def get_out_fh(out_path, parameters):
756 if out_path == '-':
757 return(sys.stdout)
758 elif parameters['saveStarsHH']:
759 out_dir = os.path.dirname(out_path)
760 if not os.path.isdir(out_dir) and out_dir != '':
761 try:
762 os.makedirs(out_dir)
763 except: # we get a WindowsError here in Windows.. pretty sure something else for Linux :D
764 log.error(("Unable to create output directory %s for HHC!") % out_dir)
765 else:
766 log.info(("Created directory '%s'") % out_dir)
767 try:
768 return(codecs.open(out_path, 'w', 'utf8'))
769 except:
770 log.error(("Output path %s couldn't be opened.") % (out_path))
771 else:
772 return(sys.stdout)