Coverage for HandHistoryConverter.py: 21%
493 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-14 11:07 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-14 11:07 +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 os
29import os.path
30import xml.dom.minidom
31import codecs
32from decimal import Decimal
34import time
35import datetime
37from pytz import timezone
38import pytz
40import logging
43import Hand
44from Exceptions import FpdbParseError, FpdbHandPartial, FpdbHandSkipped
45from abc import ABC, abstractmethod
47# logging has been set up in fpdb.py or HUD_main.py, use their settings:
48log = logging.getLogger("handHistoryConverter")
51class HandHistoryConverter(ABC):
52 READ_CHUNK_SIZE = 10000 # bytes to read at a time from file in tail mode
54 # filetype can be "text" or "xml"
55 # so far always "text"
56 # subclass HHC_xml for xml parsing
57 filetype = "text"
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"
64 re_tzOffset = re.compile("^\w+[+-]\d{4}$")
65 copyGameHeader = False
66 summaryInFile = False
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"""
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
93 self.index = index
94 self.starsArchive = starsArchive
95 self.ftpArchive = ftpArchive
97 self.in_path = in_path
98 self.base_name = self.getBasename()
99 self.out_path = out_path
100 self.kodec = None
102 self.processedHands = []
103 self.numHands = 0
104 self.numErrors = 0
105 self.numPartial = 0
106 self.isCarraige = False
107 self.autoPop = False
109 # Tourney object used to store TourneyInfo when called to deal with a Summary file
110 self.tourney = None
112 if in_path == "-":
113 self.in_fh = sys.stdin
114 self.out_fh = get_out_fh(out_path, self.import_parameters)
116 self.compiledPlayers = set()
117 self.maxseats = 0
119 self.status = True
121 self.parsedObjectType = (
122 "HH" # default behaviour : parsing HH files, can be "Summary" if the parsing encounters a Summary File
123 )
125 if autostart:
126 self.start()
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()
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
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 )
199 def setAutoPop(self, value):
200 self.autoPop = value
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)
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)
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
241 def processHand(self, handText):
242 if self.isPartial(handText):
243 raise FpdbHandPartial("Could not identify as a %s hand" % self.sitename)
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)
250 hand = None
251 game_details = None
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 print(gametype)
259 print("gametypecategory", gametype["category"])
260 if gametype["category"] in self.import_parameters["importFilters"]:
261 raise FpdbHandSkipped("Skipped %s hand" % gametype["type"])
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)
272 type = gametype["type"]
273 base = gametype["base"]
274 limit = gametype["limitType"]
275 game_details = [type, base, limit]
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
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?
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
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
308 @abstractmethod
309 def readSupportedGames(self):
310 """This method must be implemented by subclasses to define supported games."""
311 pass
313 @abstractmethod
314 def determineGameType(self, handText):
315 """This method must be implemented by subclasses to define game type determination logic."""
316 pass
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 """
332 # TODO: which parts are optional/required?
333 @abstractmethod
334 def readHandInfo(self, hand):
335 pass
337 """Read and set information about the hand being dealt, and set the correct
338 variables in the Hand object 'hand
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
347 Tournament fields:
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 """
358 # TODO: which parts are optional/required?
359 @abstractmethod
360 def readPlayerStacks(self, hand):
361 pass
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)
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'))
372 Which is pretty simple because the hand history format is consistent. Other hh formats aren't so nice.
374 This is the appropriate place to identify players that are sitting out and ignore them
376 *** NOTE: You may find this is a more appropriate place to set hand.maxseats ***
377 """
379 @abstractmethod
380 def compilePlayerRegexs(self):
381 pass
383 """Compile dynamic regexes -- compile player dependent regexes.
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.
388 eg.
389 We need to match the ante line:
390 <Player> antes $1.00
392 But <Player> is actually named
394 YesI antes $4000 - A perfectly legal playername
396 Giving:
398 YesI antes $4000 antes $1.00
400 Which without care in your regexes most people would match 'YesI' and not 'YesI antes $4000'
401 """
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
410 """For dividing the handText into sections.
412 The function requires you to pass a MatchObject with groups specifically labeled with
413 the 'correct' street names.
415 The Hand object will use the various matches for assigning actions to the correct streets.
417 Flop Based Games:
418 PREFLOP, FLOP, TURN, RIVER
420 Draw Based Games:
421 PREDEAL, DEAL, DRAWONE, DRAWTWO, DRAWTHREE
423 Stud Based Games:
424 ANTES, THIRD, FOURTH, FIFTH, SIXTH, SEVENTH
426 The Stars HHC has a good reference implementation
427 """
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
436 """Function for reading the various blinds from the hand history.
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 """
445 @abstractmethod
446 def readSTP(self, hand):
447 pass
449 @abstractmethod
450 def readAntes(self, hand):
451 pass
453 """Function for reading the antes from the hand history and passing the hand.addAnte"""
455 @abstractmethod
456 def readBringIn(self, hand):
457 pass
459 @abstractmethod
460 def readButton(self, hand):
461 pass
463 @abstractmethod
464 def readHoleCards(self, hand):
465 pass
467 @abstractmethod
468 def readAction(self, hand, street):
469 pass
471 @abstractmethod
472 def readCollectPot(self, hand):
473 pass
475 @abstractmethod
476 def readShownCards(self, hand):
477 pass
479 @abstractmethod
480 def readTourneyResults(self, hand):
481 """This function is for future use in parsing tourney results directly from a hand"""
482 pass
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
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 print("total pot", hand.totalpot)
496 print("collected pot", hand.totalcollected)
497 if hand.totalcollected > hand.totalpot:
498 print("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 print(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
543 def sanityCheck(self):
544 """Check we aren't going to do some stupid things"""
545 sane = False
546 # base_w = False
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
552 if self.in_path != "-" and self.out_path == self.in_path:
553 print(("Output and input files are the same, check config."))
554 sane = False
556 return sane
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
563 # Import from string
564 def setObs(self, text):
565 self.obs = text
566 self.whole_file = text
568 def __listof(self, x):
569 if isinstance(x, list) or isinstance(x, tuple):
570 return x
571 else:
572 return [x]
574 def readFile(self):
575 """Open in_path according to self.codepage. Exceptions caught further up"""
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
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
603 elif self.filetype == "":
604 pass
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
612 if self.maxseats > 1 and self.maxseats < 11:
613 return self.maxseats
615 mo = self.maxOccSeat(hand)
617 if mo == 10:
618 return 10 # that was easy
620 if hand.gametype["base"] == "stud":
621 if mo <= 8:
622 return 8
624 if hand.gametype["base"] == "draw":
625 if mo <= 6:
626 return 6
628 return 10
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
637 def getStatus(self):
638 # TODO: Return a status of true if file processed ok
639 return self.status
641 def getProcessedHands(self):
642 return self.processedHands
644 def getProcessedFile(self):
645 return self.out_path
647 def getLastCharacterRead(self):
648 return self.index
650 def isSummary(self, topline):
651 return " Tournament Summary " in topline
653 def getParsedObjectType(self):
654 return self.parsedObjectType
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]
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
666 def getTourney(self):
667 return self.tourney
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)
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
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])
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)
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
783 # end @staticmethod def changeTimezone
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)
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,)
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:]
841 return money.replace(",", "").replace("'", "")
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)
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)
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])
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