Coverage for HandHistoryConverter.py: 20%

459 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-27 18:50 +0000

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3 

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 

20 

21 

22from past.utils import old_div 

23#import L10n 

24#_ = L10n.get_translation() 

25 

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 

37 

38import time 

39import datetime 

40 

41from pytz import timezone 

42import pytz 

43 

44import logging 

45# logging has been set up in fpdb.py or HUD_main.py, use their settings: 

46log = logging.getLogger("parser") 

47 

48 

49import Hand 

50from Exceptions import * 

51import Configuration 

52 

53class HandHistoryConverter(object): 

54 

55 READ_CHUNK_SIZE = 10000 # bytes to read at a time from file in tail mode 

56 

57 # filetype can be "text" or "xml" 

58 # so far always "text" 

59 # subclass HHC_xml for xml parsing 

60 filetype = "text" 

61 

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" 

66 

67 re_tzOffset = re.compile('^\w+[+-]\d{4}$') 

68 copyGameHeader = False 

69 summaryInFile = False 

70 

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""" 

78 

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 

84 

85 self.index = index 

86 self.starsArchive = starsArchive 

87 self.ftpArchive = ftpArchive 

88 

89 self.in_path = in_path 

90 self.base_name = self.getBasename() 

91 self.out_path = out_path 

92 self.kodec = None 

93 

94 self.processedHands = [] 

95 self.numHands = 0 

96 self.numErrors = 0 

97 self.numPartial = 0 

98 self.isCarraige = False 

99 self.autoPop = False 

100 

101 # Tourney object used to store TourneyInfo when called to deal with a Summary file 

102 self.tourney = None 

103 

104 if in_path == '-': 

105 self.in_fh = sys.stdin 

106 self.out_fh = get_out_fh(out_path, self.import_parameters) 

107 

108 self.compiledPlayers = set() 

109 self.maxseats = 0 

110 

111 self.status = True 

112 

113 self.parsedObjectType = "HH" #default behaviour : parsing HH files, can be "Summary" if the parsing encounters a Summary File 

114 

115 

116 if autostart: 

117 self.start() 

118 

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() 

126 

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 

133 

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)) 

182 

183 def setAutoPop(self, value): 

184 self.autoPop = value 

185 

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) 

204 

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) 

209 

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 

224 

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] 

255 

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 

266 

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? 

273 

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 

281 

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 

286 

287 def readSupportedGames(self): abstract 

288 

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? 

308 

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 

312 

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 

319 

320 Tournament fields: 

321 

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? 

331 

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) 

335 

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')) 

341 

342 Which is pretty simple because the hand history format is consistent. Other hh formats aren't so nice. 

343 

344 This is the appropriate place to identify players that are sitting out and ignore them 

345 

346 *** NOTE: You may find this is a more appropriate place to set hand.maxseats *** 

347 """ 

348 

349 def compilePlayerRegexs(self): abstract 

350 """Compile dynamic regexes -- compile player dependent regexes. 

351 

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. 

354 

355 eg. 

356 We need to match the ante line: 

357 <Player> antes $1.00 

358 

359 But <Player> is actually named 

360 

361 YesI antes $4000 - A perfectly legal playername 

362 

363 Giving: 

364 

365 YesI antes $4000 antes $1.00 

366 

367 Which without care in your regexes most people would match 'YesI' and not 'YesI antes $4000' 

368 """ 

369 

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. 

375 

376 The function requires you to pass a MatchObject with groups specifically labeled with 

377 the 'correct' street names. 

378 

379 The Hand object will use the various matches for assigning actions to the correct streets. 

380 

381 Flop Based Games: 

382 PREFLOP, FLOP, TURN, RIVER 

383 

384 Draw Based Games: 

385 PREDEAL, DEAL, DRAWONE, DRAWTWO, DRAWTHREE 

386 

387 Stud Based Games: 

388 ANTES, THIRD, FOURTH, FIFTH, SIXTH, SEVENTH 

389 

390 The Stars HHC has a good reference implementation 

391 """ 

392 

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. 

398 

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 

417 

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 

423 

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 

454 

455 def sanityCheck(self): 

456 """Check we aren't going to do some stupid things""" 

457 sane = False 

458 base_w = False 

459 

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 

463 

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 

467 

468 return sane 

469 

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 

474 

475 # Import from string 

476 def setObs(self, text): 

477 self.obs = text 

478 self.whole_file = text 

479 

480 def __listof(self, x): 

481 if isinstance(x, list) or isinstance(x, tuple): 

482 return x 

483 else: 

484 return [x] 

485 

486 def readFile(self): 

487 """Open in_path according to self.codepage. Exceptions caught further up""" 

488 

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 

511 

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 

517 

518 if self.maxseats > 1 and self.maxseats < 11: 

519 return self.maxseats 

520 

521 mo = self.maxOccSeat(hand) 

522 

523 if mo == 10: return 10 #that was easy 

524 

525 if hand.gametype['base'] == 'stud': 

526 if mo <= 8: return 8 

527 

528 if hand.gametype['base'] == 'draw': 

529 if mo <= 6: return 6 

530 

531 return 10 

532 

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 

539 

540 def getStatus(self): 

541 #TODO: Return a status of true if file processed ok 

542 return self.status 

543 

544 def getProcessedHands(self): 

545 return self.processedHands 

546 

547 def getProcessedFile(self): 

548 return self.out_path 

549 

550 def getLastCharacterRead(self): 

551 return self.index 

552 

553 def isSummary(self, topline): 

554 return " Tournament Summary " in topline 

555 

556 def getParsedObjectType(self): 

557 return self.parsedObjectType 

558 

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] 

563 

564 #returns a status (True/False) indicating wether the parsing could be done correctly or not 

565 def readSummaryInfo(self, summaryInfoList): abstract 

566 

567 def getTourney(self): 

568 return self.tourney 

569 

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) 

584 

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 

591 

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]) 

667 

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) 

673 

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 

679 

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) 

687 

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, ) 

694 

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:] 

735 

736 return money.replace(',', '').replace("'", '') 

737 

738 

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) 

742 

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) 

746 

747 

748 

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]) 

754 

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)