Coverage for HandHistoryConverter.py: 21%

493 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2024-11-18 00:10 +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 os 

29import os.path 

30import xml.dom.minidom 

31import codecs 

32from decimal import Decimal 

33 

34import time 

35import datetime 

36 

37from pytz import timezone 

38import pytz 

39 

40import logging 

41 

42 

43import Hand 

44from Exceptions import FpdbParseError, FpdbHandPartial, FpdbHandSkipped 

45from abc import ABC, abstractmethod 

46 

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

48log = logging.getLogger("handHistoryConverter") 

49 

50 

51class HandHistoryConverter(ABC): 

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

53 

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

55 # so far always "text" 

56 # subclass HHC_xml for xml parsing 

57 filetype = "text" 

58 

59 # codepage indicates the encoding of the text file. 

60 # cp1252 is a safe default 

61 # "utf_8" is more likely if there are funny characters 

62 codepage = "cp1252" 

63 

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

65 copyGameHeader = False 

66 summaryInFile = False 

67 

68 # maybe archive params should be one archive param, then call method in specific converter. if archive: convert_archive() 

69 def __init__( 

70 self, 

71 config, 

72 in_path="-", 

73 out_path="-", 

74 index=0, 

75 autostart=True, 

76 starsArchive=False, 

77 ftpArchive=False, 

78 sitename="PokerStars", 

79 ): 

80 """\ 

81in_path (default '-' = sys.stdin) 

82out_path (default '-' = sys.stdout) 

83""" 

84 

85 self.config = config 

86 self.import_parameters = self.config.get_import_parameters() 

87 self.sitename = sitename 

88 log.info( 

89 "HandHistory init - %s site, %s subclass, in_path '%r'; out_path '%r'" 

90 % (self.sitename, self.__class__, in_path, out_path) 

91 ) # should use self.filter, not self.sitename 

92 

93 self.index = index 

94 self.starsArchive = starsArchive 

95 self.ftpArchive = ftpArchive 

96 

97 self.in_path = in_path 

98 self.base_name = self.getBasename() 

99 self.out_path = out_path 

100 self.kodec = None 

101 

102 self.processedHands = [] 

103 self.numHands = 0 

104 self.numErrors = 0 

105 self.numPartial = 0 

106 self.isCarraige = False 

107 self.autoPop = False 

108 

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

110 self.tourney = None 

111 

112 if in_path == "-": 

113 self.in_fh = sys.stdin 

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

115 

116 self.compiledPlayers = set() 

117 self.maxseats = 0 

118 

119 self.status = True 

120 

121 self.parsedObjectType = ( 

122 "HH" # default behaviour : parsing HH files, can be "Summary" if the parsing encounters a Summary File 

123 ) 

124 

125 if autostart: 

126 self.start() 

127 

128 def __str__(self): 

129 return """ 

130HandHistoryConverter: '%(sitename)s'  

131 filetype '%(filetype)s' 

132 in_path '%(in_path)s' 

133 out_path '%(out_path)s' 

134 """ % locals() 

135 

136 def start(self): 

137 """Process a hand at a time from the input specified by in_path.""" 

138 starttime = time.time() 

139 if not self.sanityCheck(): 

140 log.warning(("Failed sanity check")) 

141 return 

142 

143 self.numHands = 0 

144 self.numPartial = 0 

145 self.numSkipped = 0 

146 self.numErrors = 0 

147 lastParsed = None 

148 handsList = self.allHandsAsList() 

149 log.debug(("Hands list is:") + str(handsList)) 

150 log.info(("Parsing %d hands") % len(list(handsList))) 

151 # Determine if we're dealing with a HH file or a Summary file 

152 # quick fix : empty files make the handsList[0] fail ==> If empty file, go on with HH parsing 

153 if len(list(handsList)) == 0 or self.isSummary(handsList[0]) is False: 

154 self.parsedObjectType = "HH" 

155 for handText in handsList: 

156 try: 

157 self.processedHands.append(self.processHand(handText)) 

158 lastParsed = "stored" 

159 except FpdbHandPartial as e: 

160 self.numPartial += 1 

161 lastParsed = "partial" 

162 log.debug("%s" % e) 

163 except FpdbHandSkipped: 

164 self.numSkipped += 1 

165 lastParsed = "skipped" 

166 except FpdbParseError: 

167 self.numErrors += 1 

168 lastParsed = "error" 

169 log.error(("FpdbParseError for file '%s'") % self.in_path) 

170 if lastParsed in ("partial", "error") and self.autoPop: 

171 self.index -= len(handsList[-1]) 

172 if self.isCarraige: 

173 self.index -= handsList[-1].count("\n") 

174 handsList.pop() 

175 if lastParsed == "partial": 

176 self.numPartial -= 1 

177 else: 

178 self.numErrors -= 1 

179 log.info(("Removing partially written hand & resetting index")) 

180 self.numHands = len(list(handsList)) 

181 endtime = time.time() 

182 log.info( 

183 ("Read %d hands (%d failed) in %.3f seconds") 

184 % (self.numHands, (self.numErrors + self.numPartial), endtime - starttime) 

185 ) 

186 else: 

187 self.parsedObjectType = "Summary" 

188 summaryParsingStatus = self.readSummaryInfo(handsList) 

189 endtime = time.time() 

190 if summaryParsingStatus: 

191 log.info( 

192 ("Summary file '%s' correctly parsed (took %.3f seconds)") % (self.in_path, endtime - starttime) 

193 ) 

194 else: 

195 log.warning( 

196 ("Error converting summary file '%s' (took %.3f seconds)") % (self.in_path, endtime - starttime) 

197 ) 

198 

199 def setAutoPop(self, value): 

200 self.autoPop = value 

201 

202 def allHandsAsList(self): 

203 """Return a list of handtexts in the file at self.in_path""" 

204 # 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. 

205 self.readFile() 

206 lenobs = len(self.obs) 

207 self.obs = self.obs.rstrip() 

208 self.index -= lenobs - len(self.obs) 

209 self.obs = self.obs.lstrip() 

210 lenobs = len(self.obs) 

211 self.obs = self.obs.replace("\r\n", "\n").replace("\xa0", " ") 

212 if lenobs != len(self.obs): 

213 self.isCarraige = True 

214 # maybe archive params should be one archive param, then call method in specific converter? 

215 # if self.archive: 

216 # self.obs = self.convert_archive(self.obs) 

217 if self.starsArchive is True: 

218 m = re.compile("^Hand #\d+", re.MULTILINE) 

219 self.obs = m.sub("", self.obs) 

220 

221 if self.ftpArchive is True: 

222 # Remove ******************** # 1 ************************* 

223 m = re.compile("\*{20}\s#\s\d+\s\*{20,25}\s+", re.MULTILINE) 

224 self.obs = m.sub("", self.obs) 

225 

226 if self.obs is None or self.obs == "": 

227 log.info(("Read no hands from file: '%s'") % self.in_path) 

228 return [] 

229 handlist = re.split(self.re_SplitHands, self.obs) 

230 # Some HH formats leave dangling text after the split 

231 # ie. </game> (split) </session>EOL 

232 # Remove this dangler if less than 50 characters and warn in the log 

233 if len(handlist[-1]) <= 50: 

234 self.index -= len(handlist[-1]) 

235 if self.isCarraige: 

236 self.index -= handlist[-1].count("\n") 

237 handlist.pop() 

238 log.info(("Removing text < 50 characters & resetting index")) 

239 return handlist 

240 

241 def processHand(self, handText): 

242 if self.isPartial(handText): 

243 raise FpdbHandPartial("Could not identify as a %s hand" % self.sitename) 

244 

245 if self.copyGameHeader: 

246 gametype = self.parseHeader(handText, self.whole_file.replace("\r\n", "\n").replace("\xa0", " ")) 

247 else: 

248 gametype = self.determineGameType(handText) 

249 

250 hand = None 

251 game_details = None 

252 

253 if gametype is None: 

254 gametype = "unmatched" 

255 # TODO: not ideal, just trying to not error. Throw ParseException? 

256 self.numErrors += 1 

257 else: 

258 log.debug(gametype) 

259 log.debug("gametypecategory", gametype["category"]) 

260 if gametype["category"] in self.import_parameters["importFilters"]: 

261 raise FpdbHandSkipped("Skipped %s hand" % gametype["type"]) 

262 

263 # Ensure game type has all necessary attributes 

264 gametype.setdefault("mix", "none") 

265 gametype.setdefault("ante", 0) 

266 gametype.setdefault("buyinType", "regular") 

267 gametype.setdefault("fast", False) 

268 gametype.setdefault("newToGame", False) 

269 gametype.setdefault("homeGame", False) 

270 gametype.setdefault("split", False) 

271 

272 type = gametype["type"] 

273 base = gametype["base"] 

274 limit = gametype["limitType"] 

275 game_details = [type, base, limit] 

276 

277 if game_details in self.readSupportedGames(): 

278 if gametype["base"] == "hold": 

279 hand = Hand.HoldemOmahaHand(self.config, self, self.sitename, gametype, handText) 

280 elif gametype["base"] == "stud": 

281 hand = Hand.StudHand(self.config, self, self.sitename, gametype, handText) 

282 elif gametype["base"] == "draw": 

283 hand = Hand.DrawHand(self.config, self, self.sitename, gametype, handText) 

284 else: 

285 log.error("%s Unsupported game type: %s", self.sitename, gametype) 

286 raise FpdbParseError 

287 

288 if hand: 

289 # hand.writeHand(self.out_fh) 

290 return hand 

291 else: 

292 log.error("%s Unsupported game type: %s", self.sitename, gametype) 

293 # TODO: pity we don't know the HID at this stage. Log the entire hand? 

294 

295 def isPartial(self, handText): 

296 count = 0 

297 for m in self.re_Identify.finditer(handText): 

298 count += 1 

299 if count != 1: 

300 return True 

301 return False 

302 

303 # These functions are parse actions that may be overridden by the inheriting class 

304 # This function should return a list of lists looking like: 

305 # return [["ring", "hold", "nl"], ["tour", "hold", "nl"]] 

306 # Showing all supported games limits and types 

307 

308 @abstractmethod 

309 def readSupportedGames(self): 

310 """This method must be implemented by subclasses to define supported games.""" 

311 pass 

312 

313 @abstractmethod 

314 def determineGameType(self, handText): 

315 """This method must be implemented by subclasses to define game type determination logic.""" 

316 pass 

317 

318 """return dict with keys/values: 

319 'type' in ('ring', 'tour') 

320 'limitType' in ('nl', 'cn', 'pl', 'cp', 'fl') 

321 'base' in ('hold', 'stud', 'draw') 

322 'category' in ('holdem', 'omahahi', omahahilo', 'fusion', 'razz', 'studhi', 'studhilo', 'fivedraw', '27_1draw', '27_3draw', 'badugi') 

323 'hilo' in ('h','l','s') 

324 'mix' in (site specific, or 'none') 

325 'smallBlind' int? 

326 'bigBlind' int? 

327 'smallBet' 

328 'bigBet' 

329 'currency' in ('USD', 'EUR', 'T$', <countrycode>) 

330or None if we fail to get the info """ 

331 

332 # TODO: which parts are optional/required? 

333 @abstractmethod 

334 def readHandInfo(self, hand): 

335 pass 

336 

337 """Read and set information about the hand being dealt, and set the correct  

338 variables in the Hand object 'hand 

339 

340 * hand.startTime - a datetime object 

341 * hand.handid - The site identified for the hand - a string. 

342 * hand.tablename 

343 * hand.buttonpos 

344 * hand.maxseats 

345 * hand.mixed 

346 

347 Tournament fields: 

348 

349 * hand.tourNo - The site identified tournament id as appropriate - a string. 

350 * hand.buyin 

351 * hand.fee 

352 * hand.buyinCurrency 

353 * hand.koBounty 

354 * hand.isKO 

355 * hand.level 

356 """ 

357 

358 # TODO: which parts are optional/required? 

359 @abstractmethod 

360 def readPlayerStacks(self, hand): 

361 pass 

362 

363 """This function is for identifying players at the table, and to pass the  

364 information on to 'hand' via Hand.addPlayer(seat, name, chips) 

365 

366 At the time of writing the reference function in the PS converter is: 

367 log.debug("readPlayerStacks") 

368 m = self.re_PlayerInfo.finditer(hand.handText) 

369 for a in m: 

370 hand.addPlayer(int(a.group('SEAT')), a.group('PNAME'), a.group('CASH')) 

371 

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

373 

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

375 

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

377 """ 

378 

379 @abstractmethod 

380 def compilePlayerRegexs(self): 

381 pass 

382 

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

384 

385 Depending on the ambiguity of lines you may need to match, and the complexity of  

386 player names - we found that we needed to recompile some regexes for player actions so that they actually contained the player names. 

387 

388 eg. 

389 We need to match the ante line: 

390 <Player> antes $1.00 

391 

392 But <Player> is actually named 

393 

394 YesI antes $4000 - A perfectly legal playername 

395 

396 Giving: 

397 

398 YesI antes $4000 antes $1.00 

399 

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

401 """ 

402 

403 # Needs to return a MatchObject with group names identifying the streets into the Hand object 

404 # so groups are called by street names 'PREFLOP', 'FLOP', 'STREET2' etc 

405 # blinds are done seperately 

406 @abstractmethod 

407 def markStreets(self, hand): 

408 pass 

409 

410 """For dividing the handText into sections. 

411 

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

413 the 'correct' street names. 

414 

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

416 

417 Flop Based Games: 

418 PREFLOP, FLOP, TURN, RIVER 

419 

420 Draw Based Games: 

421 PREDEAL, DEAL, DRAWONE, DRAWTWO, DRAWTHREE 

422 

423 Stud Based Games: 

424 ANTES, THIRD, FOURTH, FIFTH, SIXTH, SEVENTH 

425 

426 The Stars HHC has a good reference implementation 

427 """ 

428 

429 # Needs to return a list in the format 

430 # ['player1name', 'player2name', ...] where player1name is the sb and player2name is bb, 

431 # addtional players are assumed to post a bb oop 

432 @abstractmethod 

433 def readBlinds(self, hand): 

434 pass 

435 

436 """Function for reading the various blinds from the hand history. 

437 

438 Pass any small blind to hand.addBlind(<name>, "small blind", <value>) 

439 - unless it is a single dead small blind then use: 

440 hand.addBlind(<name>, 'secondsb', <value>) 

441 Pass any big blind to hand.addBlind(<name>, "big blind", <value>) 

442 Pass any play posting both big and small blinds to hand.addBlind(<name>, 'both', <vale>) 

443 """ 

444 

445 @abstractmethod 

446 def readSTP(self, hand): 

447 pass 

448 

449 @abstractmethod 

450 def readAntes(self, hand): 

451 pass 

452 

453 """Function for reading the antes from the hand history and passing the hand.addAnte""" 

454 

455 @abstractmethod 

456 def readBringIn(self, hand): 

457 pass 

458 

459 @abstractmethod 

460 def readButton(self, hand): 

461 pass 

462 

463 @abstractmethod 

464 def readHoleCards(self, hand): 

465 pass 

466 

467 @abstractmethod 

468 def readAction(self, hand, street): 

469 pass 

470 

471 @abstractmethod 

472 def readCollectPot(self, hand): 

473 pass 

474 

475 @abstractmethod 

476 def readShownCards(self, hand): 

477 pass 

478 

479 @abstractmethod 

480 def readTourneyResults(self, hand): 

481 """This function is for future use in parsing tourney results directly from a hand""" 

482 pass 

483 

484 # EDIT: readOther is depreciated 

485 # Some sites do odd stuff that doesn't fall in to the normal HH parsing. 

486 # e.g., FTP doesn't put mixed game info in the HH, but puts in in the 

487 # file name. Use readOther() to clean up those messes. 

488 # @abstractmethod 

489 # def readOther(self, hand): 

490 # pass 

491 

492 # Some sites don't report the rake. This will be called at the end of the hand after the pot total has been calculated 

493 # an inheriting class can calculate it for the specific site if need be. 

494 def getRake(self, hand): 

495 log.debug("total pot", hand.totalpot) 

496 log.debug("collected pot", hand.totalcollected) 

497 if hand.totalcollected > hand.totalpot: 

498 log.debug("collected pot>total pot") 

499 if hand.rake is None: 

500 hand.rake = hand.totalpot - hand.totalcollected # * Decimal('0.05') # probably not quite right 

501 if self.siteId == 9 and hand.gametype["type"] == "tour": 

502 round = -5 # round up to 10 

503 elif hand.gametype["type"] == "tour": 

504 round = -1 

505 else: 

506 round = -0.01 

507 if self.siteId == 15 and hand.totalcollected > hand.totalpot: 

508 hand.rake = old_div(hand.totalpot, 10) 

509 log.debug(hand.rake) 

510 if hand.rake < 0 and (not hand.roundPenny or hand.rake < round) and not hand.cashedOut: 

511 if self.siteId == 28 and ( 

512 (hand.rake + Decimal(str(hand.sb)) - (0 if hand.rakes.get("rake") is None else hand.rakes["rake"])) == 0 

513 or ( 

514 hand.rake 

515 + Decimal(str(hand.sb)) 

516 + Decimal(str(hand.bb)) 

517 - (0 if hand.rakes.get("rake") is None else hand.rakes["rake"]) 

518 ) 

519 == 0 

520 ): 

521 log.error( 

522 ("hhc.getRake(): '%s': Missed sb/bb - Amount collected (%s) is greater than the pot (%s)") 

523 % (hand.handid, str(hand.totalcollected), str(hand.totalpot)) 

524 ) 

525 else: 

526 log.error( 

527 ("hhc.getRake(): '%s': Amount collected (%s) is greater than the pot (%s)") 

528 % (hand.handid, str(hand.totalcollected), str(hand.totalpot)) 

529 ) 

530 raise FpdbParseError 

531 elif ( 

532 hand.totalpot > 0 

533 and Decimal(old_div(hand.totalpot, 4)) < hand.rake 

534 and not hand.fastFold 

535 and not hand.cashedOut 

536 ): 

537 log.error( 

538 ("hhc.getRake(): '%s': Suspiciously high rake (%s) > 25 pct of pot (%s)") 

539 % (hand.handid, str(hand.rake), str(hand.totalpot)) 

540 ) 

541 raise FpdbParseError 

542 

543 def sanityCheck(self): 

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

545 sane = False 

546 # base_w = False 

547 

548 # Make sure input and output files are different or we'll overwrite the source file 

549 if True: # basically.. I don't know 

550 sane = True 

551 

552 if self.in_path != "-" and self.out_path == self.in_path: 

553 log.debug(("Output and input files are the same, check config.")) 

554 sane = False 

555 

556 return sane 

557 

558 # Functions not necessary to implement in sub class 

559 def setFileType(self, filetype="text", codepage="utf8"): 

560 self.filetype = filetype 

561 self.codepage = codepage 

562 

563 # Import from string 

564 def setObs(self, text): 

565 self.obs = text 

566 self.whole_file = text 

567 

568 def __listof(self, x): 

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

570 return x 

571 else: 

572 return [x] 

573 

574 def readFile(self): 

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

576 

577 if self.filetype == "text": 

578 for kodec in self.__listof(self.codepage): 

579 # print "trying", kodec 

580 try: 

581 in_fh = codecs.open(self.in_path, "r", kodec) 

582 self.whole_file = in_fh.read() 

583 in_fh.close() 

584 self.obs = self.whole_file[self.index :] 

585 self.index = len(self.whole_file) 

586 self.kodec = kodec 

587 return True 

588 except (IOError, UnicodeDecodeError) as e: 

589 log.warning(f"Failed to read file with codec {kodec}: {e}") 

590 else: 

591 log.error(f"Unable to read file with any codec in list! {self.in_path}") 

592 self.obs = "" 

593 return False 

594 

595 elif self.filetype == "xml": 

596 if hasattr(self, "in_path"): # Ensure filename (in_path) is available 

597 doc = xml.dom.minidom.parse(self.in_path) 

598 self.doc = doc 

599 else: 

600 log.error("No file path provided for XML filetype") 

601 return False 

602 

603 elif self.filetype == "": 

604 pass 

605 

606 def guessMaxSeats(self, hand): 

607 """Return a guess at maxseats when not specified in HH.""" 

608 # if some other code prior to this has already set it, return it 

609 if not self.copyGameHeader and hand.gametype["type"] == "tour": 

610 return 10 

611 

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

613 return self.maxseats 

614 

615 mo = self.maxOccSeat(hand) 

616 

617 if mo == 10: 

618 return 10 # that was easy 

619 

620 if hand.gametype["base"] == "stud": 

621 if mo <= 8: 

622 return 8 

623 

624 if hand.gametype["base"] == "draw": 

625 if mo <= 6: 

626 return 6 

627 

628 return 10 

629 

630 def maxOccSeat(self, hand): 

631 max = 0 

632 for player in hand.players: 

633 if int(player[0]) > max: 

634 max = int(player[0]) 

635 return max 

636 

637 def getStatus(self): 

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

639 return self.status 

640 

641 def getProcessedHands(self): 

642 return self.processedHands 

643 

644 def getProcessedFile(self): 

645 return self.out_path 

646 

647 def getLastCharacterRead(self): 

648 return self.index 

649 

650 def isSummary(self, topline): 

651 return " Tournament Summary " in topline 

652 

653 def getParsedObjectType(self): 

654 return self.parsedObjectType 

655 

656 def getBasename(self): 

657 head, tail = os.path.split(self.in_path) 

658 base = tail or os.path.basename(head) 

659 return base.split(".")[0] 

660 

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

662 @abstractmethod 

663 def readSummaryInfo(self, summaryInfoList): 

664 pass 

665 

666 def getTourney(self): 

667 return self.tourney 

668 

669 @staticmethod 

670 def changeTimezone(time, givenTimezone, wantedTimezone): 

671 """Takes a givenTimezone in format AAA or AAA+HHMM where AAA is a standard timezone 

672 and +HHMM is an optional offset (+/-) in hours (HH) and minutes (MM) 

673 (See OnGameToFpdb.py for example use of the +HHMM part) 

674 Tries to convert the time parameter (with no timezone) from the givenTimezone to 

675 the wantedTimeZone (currently only allows "UTC") 

676 """ 

677 # log.debug("raw time: " + str(time) + " given time zone: " + str(givenTimezone)) 

678 if wantedTimezone == "UTC": 

679 wantedTimezone = pytz.utc 

680 else: 

681 log.error(("Unsupported target timezone: ") + givenTimezone) 

682 raise FpdbParseError(("Unsupported target timezone: ") + givenTimezone) 

683 

684 givenTZ = None 

685 if HandHistoryConverter.re_tzOffset.match(givenTimezone): 

686 offset = int(givenTimezone[-5:]) 

687 givenTimezone = givenTimezone[0:-5] 

688 # log.debug("changeTimeZone: offset=" + str(offset)) 

689 else: 

690 offset = 0 

691 

692 if givenTimezone in ("ET", "EST", "EDT"): 

693 givenTZ = timezone("US/Eastern") 

694 elif givenTimezone in ("CET", "CEST", "MEZ", "MESZ", "HAEC"): 

695 # since CEST will only be used in summer time it's ok to treat it as identical to CET. 

696 givenTZ = timezone("Europe/Berlin") 

697 # Note: Daylight Saving Time is standardised across the EU so this should be fine 

698 elif givenTimezone in ("GT", "GMT"): # GMT is always the same as UTC 

699 givenTZ = timezone("GMT") 

700 # GMT cannot be treated as WET because some HH's are explicitly 

701 # GMT+-delta so would be incorrect during the summertime 

702 # if substituted as WET+-delta 

703 elif givenTimezone == "BST": 

704 givenTZ = timezone("Europe/London") 

705 elif givenTimezone == "WET": # WET is GMT with daylight saving delta 

706 givenTZ = timezone("WET") 

707 elif givenTimezone in ("HT", "HST", "HDT"): # Hawaiian Standard Time 

708 givenTZ = timezone("US/Hawaii") 

709 elif givenTimezone == "AKT": # Alaska Time 

710 givenTZ = timezone("US/Alaska") 

711 elif givenTimezone in ("PT", "PST", "PDT"): # Pacific Time 

712 givenTZ = timezone("US/Pacific") 

713 elif givenTimezone in ("MT", "MST", "MDT"): # Mountain Time 

714 givenTZ = timezone("US/Mountain") 

715 elif givenTimezone in ("CT", "CST", "CDT"): # Central Time 

716 givenTZ = timezone("US/Central") 

717 elif givenTimezone == "AT": # Atlantic Time 

718 givenTZ = timezone("Canada/Atlantic") 

719 elif givenTimezone == "NT": # Newfoundland Time 

720 givenTZ = timezone("Canada/Newfoundland") 

721 elif givenTimezone == "ART": # Argentinian Time 

722 givenTZ = timezone("America/Argentina/Buenos_Aires") 

723 elif givenTimezone in ("BRT", "BRST"): # Brasilia Time 

724 givenTZ = timezone("America/Sao_Paulo") 

725 elif givenTimezone == "VET": 

726 givenTZ = timezone("America/Caracas") 

727 elif givenTimezone == "COT": 

728 givenTZ = timezone("America/Bogota") 

729 elif givenTimezone in ("EET", "EEST"): # Eastern European Time 

730 givenTZ = timezone("Europe/Bucharest") 

731 elif givenTimezone in ("MSK", "MESZ", "MSKS", "MSD"): # Moscow Standard Time 

732 givenTZ = timezone("Europe/Moscow") 

733 elif givenTimezone == "GST": 

734 givenTZ = timezone("Asia/Dubai") 

735 elif givenTimezone in ("YEKT", "YEKST"): 

736 givenTZ = timezone("Asia/Yekaterinburg") 

737 elif givenTimezone in ("KRAT", "KRAST"): 

738 givenTZ = timezone("Asia/Krasnoyarsk") 

739 elif givenTimezone == "IST": # India Standard Time 

740 givenTZ = timezone("Asia/Kolkata") 

741 elif givenTimezone == "ICT": 

742 givenTZ = timezone("Asia/Bangkok") 

743 elif givenTimezone == "CCT": # China Coast Time 

744 givenTZ = timezone("Australia/West") 

745 elif givenTimezone == "JST": # Japan Standard Time 

746 givenTZ = timezone("Asia/Tokyo") 

747 elif givenTimezone in ("AWST", "AWT"): # Australian Western Standard Time 

748 givenTZ = timezone("Australia/West") 

749 elif givenTimezone in ("ACST", "ACT"): # Australian Central Standard Time 

750 givenTZ = timezone("Australia/Darwin") 

751 elif givenTimezone in ("AEST", "AET"): # Australian Eastern Standard Time 

752 # Each State on the East Coast has different DSTs. 

753 # Melbournce is out because I don't like AFL, Queensland doesn't have DST 

754 # ACT is full of politicians and Tasmania will never notice. 

755 # Using Sydney. 

756 givenTZ = timezone("Australia/Sydney") 

757 elif givenTimezone in ("NZST", "NZT", "NZDT"): # New Zealand Time 

758 givenTZ = timezone("Pacific/Auckland") 

759 elif givenTimezone == "UTC": # Universal time co-ordinated 

760 givenTZ = pytz.UTC 

761 elif givenTimezone in pytz.all_timezones: 

762 givenTZ = timezone(givenTimezone) 

763 else: 

764 timezone_lookup = dict( 

765 [(pytz.timezone(x).localize(datetime.datetime.now()).tzname(), x) for x in pytz.all_timezones] 

766 ) 

767 if givenTimezone in timezone_lookup: 

768 givenTZ = timezone(timezone_lookup[givenTimezone]) 

769 

770 if givenTZ is None: 

771 # do not crash if timezone not in list, just return UTC localized time 

772 log.error(("Timezone conversion not supported") + ": " + givenTimezone + " " + str(time)) 

773 givenTZ = pytz.UTC 

774 return givenTZ.localize(time) 

775 

776 localisedTime = givenTZ.localize(time) 

777 utcTime = localisedTime.astimezone(wantedTimezone) + datetime.timedelta( 

778 seconds=-3600 * (old_div(offset, 100)) - 60 * (offset % 100) 

779 ) 

780 # log.debug("utcTime: " + str(utcTime)) 

781 return utcTime 

782 

783 # end @staticmethod def changeTimezone 

784 

785 @staticmethod 

786 def getTableTitleRe(type, table_name=None, tournament=None, table_number=None): 

787 "Returns string to search in windows titles" 

788 if type == "tour": 

789 return re.escape(str(tournament)) + ".+\\Table " + re.escape(str(table_number)) 

790 else: 

791 return re.escape(table_name) 

792 

793 @staticmethod 

794 def getTableNoRe(tournament): 

795 "Returns string to search window title for tournament table no." 

796 # Full Tilt: $30 + $3 Tournament (181398949), Table 1 - 600/1200 Ante 100 - Limit Razz 

797 # PokerStars: WCOOP 2nd Chance 02: $1,050 NLHE - Tournament 307521826 Table 1 - Blinds $30/$60 

798 return "%s.+(?:Table|Torneo) (\d+)" % (tournament,) 

799 

800 @staticmethod 

801 def clearMoneyString(money): 

802 """Converts human readable string representations of numbers like 

803 '1 200', '2,000', '0,01' to more machine processable form - no commas, 1 decimal point 

804 """ 

805 if not money: 

806 return money 

807 money = money.replace(" ", "") 

808 money = money.replace("\xa0", "") 

809 if "K" in money: 

810 money = money.replace("K", "000") 

811 if "M" in money: 

812 money = money.replace("M", "000000") 

813 if "B" in money: 

814 money = money.replace("B", "000000000") 

815 if money[-1] in (".", ","): 

816 money = money[:-1] 

817 if len(money) < 3: 

818 return money # No commas until 0,01 or 1,00 

819 if money[-3] == ",": 

820 money = money[:-3] + "." + money[-2:] 

821 if len(money) > 15: 

822 if money[-15] == ".": 

823 money = money[:-15] + "," + money[-14:] 

824 if len(money) > 11: 

825 if money[-11] == ".": 

826 money = money[:-11] + "," + money[-10:] 

827 if len(money) > 7: 

828 if money[-7] == ".": 

829 money = money[:-7] + "," + money[-6:] 

830 else: 

831 if len(money) > 12: 

832 if money[-12] == ".": 

833 money = money[:-12] + "," + money[-11:] 

834 if len(money) > 8: 

835 if money[-8] == ".": 

836 money = money[:-8] + "," + money[-7:] 

837 if len(money) > 4: 

838 if money[-4] == ".": 

839 money = money[:-4] + "," + money[-3:] 

840 

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

842 

843 

844def getTableTitleRe(config, sitename, *args, **kwargs): 

845 "Returns string to search in windows titles for current site" 

846 return getSiteHhc(config, sitename).getTableTitleRe(*args, **kwargs) 

847 

848 

849def getTableNoRe(config, sitename, *args, **kwargs): 

850 "Returns string to search window titles for tournament table no." 

851 return getSiteHhc(config, sitename).getTableNoRe(*args, **kwargs) 

852 

853 

854def getSiteHhc(config, sitename): 

855 "Returns HHC class for current site" 

856 hhcName = config.hhcs[sitename].converter 

857 hhcModule = __import__(hhcName) 

858 return getattr(hhcModule, hhcName[:-6]) 

859 

860 

861def get_out_fh(out_path, parameters): 

862 if out_path == "-": 

863 return sys.stdout 

864 elif parameters.get("saveStarsHH", False): 

865 out_dir = os.path.dirname(out_path) 

866 if not os.path.isdir(out_dir) and out_dir != "": 

867 try: 

868 os.makedirs(out_dir) 

869 except OSError as e: 

870 log.error(f"Unable to create output directory {out_dir} for HHC: {e}") 

871 else: 

872 log.info(f"Created directory '{out_dir}'") 

873 try: 

874 return codecs.open(out_path, "w", "utf8") 

875 except (IOError, OSError) as e: 

876 log.error(f"Output path {out_path} couldn't be opened: {e}") 

877 return None 

878 else: 

879 return sys.stdout