diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..89cac29 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "ir_webstats"] + path = ir_webstats + url = https://github.com/lowlyocean/ir_webstats.git +[submodule "pyirsdk"] + path = pyirsdk + url = https://github.com/kutu/pyirsdk.git diff --git a/constants.py b/constants.py deleted file mode 100644 index 63fde11..0000000 --- a/constants.py +++ /dev/null @@ -1,329 +0,0 @@ -# _*_ coding: utf_8 _*_ - -# evttypes: -# race = 5 -# ttrial = 4 -# qual = 3 -# prac = 2 - -ALL = -1 -NUM_ENTRIES = 25 # Entries per page. This is the ammount set in iRacing site. We shouldn't increase it. -WAIT_TIME = 0.3 # Minimum time in seconds between two consecutive requests to iRacing site (we don't want to flood/abuse the service). I'm not sure about the minimum value for this, I'll have to ask a dev. - -IRATING_OVAL_CHART = 1 -IRATING_ROAD_CHART = 2 - -RACE_TYPE_OVAL = 1 -RACE_TYPE_ROAD = 2 - -LIC_ROOKIE = 1 -LIC_D = 2 -LIC_C = 3 -LIC_B = 4 -LIC_A = 5 -LIC_PRO = 6 -LIC_PRO_WC = 7 - -SORT_IRATING = 'irating' -SORT_TIME = 'start_time' -SORT_POINTS = 'points' -ORDER_DESC = 'desc' -ORDER_ASC = 'asc' - -#OTHER -EVENT_RACE = 1 -EVENT_QUALY = 2 -EVENT_PRACTICE = 3 -EVENT_TTRIAL = 4 - -EVENT_OFFICIAL = 6 -EVENT_UNOFFICIAL = 7 - -#URLS -URL_IRACING_LOGIN = 'https://members.iracing.com/membersite/login.jsp' -URL_IRACING_LOGIN2 = 'https://members.iracing.com/membersite/Login' -URL_IRACING_HOME = 'http://members.iracing.com/membersite/member/Home.do' -URL_STATS_CHART = 'http://members.iracing.com/memberstats/member/GetChartData?custId=%s&catId=%s&chartType=1' -URL_DRIVER_COUNTS = 'http://members.iracing.com/membersite/member/GetDriverCounts' -URL_CAREER_STATS = 'http://members.iracing.com/memberstats/member/GetCareerStats?custid=%s' -URL_YEARLY_STATS = 'http://members.iracing.com/memberstats/member/GetYearlyStats?custid=%s' -URL_CARS_DRIVEN = 'http://members.iracing.com/memberstats/member/GetCarsDriven?custid=%s' -URL_PERSONAL_BEST = 'http://members.iracing.com/memberstats/member/GetPersonalBests?carid=%s&custid=%s' -URL_DRIVER_STATUS = 'http://members.iracing.com/membersite/member/GetDriverStatus?%s' -URL_DRIVER_STATS = 'http://members.iracing.com/memberstats/member/GetDriverStats' -URL_LASTRACE_STATS = 'http://members.iracing.com/memberstats/member/GetLastRacesStats?custid=%s' -URL_RESULTS_ARCHIVE = 'http://members.iracing.com/memberstats/member/GetResults' -URL_SEASON_STANDINGS = 'http://members.iracing.com/memberstats/member/GetSeasonStandings' -URL_SEASON_TT_STANDINGS = 'http://members.iracing.com/memberstats/member/GetSeasonTTStandings' -URL_SEASON_TT_RES = 'http://members.iracing.com/memberstats/member/GetSeasonTTResults' -URL_SEASON_QUAL_RES = 'http://members.iracing.com/memberstats/member/GetSeasonQualifyResults' -URL_AGGREGATE_SEASON_STANDINGS = 'http://members.iracing.com/memberstats/member/GetAggregateSeasonStandings?seasonids=%s&start=%s&end=%s' -URL_HOSTED_RESULTS = 'http://members.iracing.com/memberstats/member/GetPrivateSessionResults' -URL_SELECT_SERIES = 'http://members.iracing.com/membersite/member/SelectSeries.do?&season=%s&view=undefined&nocache=%s' -URL_SESSION_TIMES = 'http://members.iracing.com/membersite/member/GetSessionTimes'#T-m-d -URL_SERIES_RACERESULTS = 'http://members.iracing.com/memberstats/member/GetSeriesRaceResults' -URL_LAST_SERIES = 'http://members.iracing.com/memberstats/member/GetLastSeries?custid=%s' - -URL_GET_EVENTRESULTS = 'http://members.iracing.com/membersite/member/GetEventResultsAsCSV?subsessionid=%s&simsesnum=%s&includeSummary=1' #simsesnum 0 race, -1 qualy or practice, -2 practice -URL_GET_EVENTRESULTS2 = 'http://members.iracing.com/membersite/member/EventResult.do?subsessionid=%s&custid=%s' -URL_GET_SUBSESSRESULTS = 'http://members.iracing.com/membersite/member/GetSubsessionResults?subsessionID=%s&custid=%s' - -URL_GET_LAPS_SINGLE = 'http://members.iracing.com/membersite/member/GetLaps?&subsessionid=%s&groupid=%s&simsessnum=%s' -URL_GET_LAPS_ALL = 'http://members.iracing.com/membersite/member/GetLapChart?&subsessionid=%s&carclassid=-1' - -URL_GET_PASTSERIES = 'http://members.iracing.com/membersite/member/PreviousSeasons.do' - -URL_GET_WORLDRECORD = 'http://members.iracing.com/memberstats/member/GetWorldRecords?seasonyear=%s&seasonquarter=%s&carid=%s&trackid=%s&custid=%s&format=json&upperbound=1' - -HEADERS = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36' - , 'Referer': 'https://members.iracing.com/membersite/login.jsp', 'Connection': 'keep-alive', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8','Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3','Cache-Control': 'max-age=0', 'Host': 'members.iracing.com','Accept-Encoding': 'gzip,deflate,sdch', 'Origin': 'members.iracing.com', 'Accept-Language': 'en-US,en;q=0.8'} - -#LOCATIONS -LOC_ALL = 'null' -LOC_AFGHANISTAN = 'AF' -LOC_ALAND_ISLANDS = 'AX' -LOC_ALBANIA = 'AL' -LOC_ALGERIA = 'DZ' -LOC_AMERICAN_SAMOA = 'AS' -LOC_ANDORRA = 'AD' -LOC_ANGOLA = 'AO' -LOC_ANGUILLA = 'AI' -LOC_ANTARCTICA = 'AQ' -LOC_ANTIGUA_AND_BARBUDA = 'AG' -LOC_ARGENTINA = 'AR' -LOC_ARMENIA = 'AM' -LOC_ARUBA = 'AW' -LOC_AUSTRALIA = 'AU' -LOC_AUSTRIA = 'AT' -LOC_AZERBAIJAN = 'AZ' -LOC_BAHAMAS = 'BS' -LOC_BAHRAIN = 'BH' -LOC_BANGLADESH = 'BD' -LOC_BARBADOS = 'BB' -LOC_BELARUS = 'BY' -LOC_BELGIUM = 'BE' -LOC_BELIZE = 'BZ' -LOC_BENIN = 'BJ' -LOC_BERMUDA = 'BM' -LOC_BHUTAN = 'BT' -LOC_BOLIVIA_PLURINATIONAL_STATE_OF = 'BO' -LOC_BOSNIA_AND_HERZEGOVINA = 'BA' -LOC_BOTSWANA = 'BW' -LOC_BOUVET_ISLAND = 'BV' -LOC_BRAZIL = 'BR' -LOC_BRITISH_INDIAN_OCEAN_TERRITORY = 'IO' -LOC_BRUNEI_DARUSSALAM = 'BN' -LOC_BULGARIA = 'BG' -LOC_BURKINA_FASO = 'BF' -LOC_BURUNDI = 'BI' -LOC_CAMBODIA = 'KH' -LOC_CAMEROON = 'CM' -LOC_CANADA = 'CA' -LOC_CAPE_VERDE = 'CV' -LOC_CAYMAN_ISLANDS = 'KY' -LOC_CENTRAL_AFRICAN_REPUBLIC = 'CF' -LOC_CHAD = 'TD' -LOC_CHILE = 'CL' -LOC_CHINA = 'CN' -LOC_CHRISTMAS_ISLAND = 'CX' -LOC_COCOS_KEELING_ISLANDS = 'CC' -LOC_COLOMBIA = 'CO' -LOC_COMOROS = 'KM' -LOC_CONGO = 'CG' -LOC_CONGO_THE_DEMOCRATIC_REPUBLIC_OF_THE = 'CD' -LOC_COOK_ISLANDS = 'CK' -LOC_COSTA_RICA = 'CR' -LOC_COTE_DIVOIRE = 'CI' -LOC_CROATIA = 'HR' -LOC_CUBA = 'CU' -LOC_CYPRUS = 'CY' -LOC_CZECH_REPUBLIC = 'CZ' -LOC_DENMARK = 'DK' -LOC_DJIBOUTI = 'DJ' -LOC_DOMINICA = 'DM' -LOC_DOMINICAN_REPUBLIC = 'DO' -LOC_ECUADOR = 'EC' -LOC_EGYPT = 'EG' -LOC_EL_SALVADOR = 'SV' -LOC_EQUATORIAL_GUINEA = 'GQ' -LOC_ERITREA = 'ER' -LOC_ESTONIA = 'EE' -LOC_ETHIOPIA = 'ET' -LOC_FALKLAND_ISLANDS_MALVINAS = 'FK' -LOC_FAROE_ISLANDS = 'FO' -LOC_FIJI = 'FJ' -LOC_FINLAND = 'FI' -LOC_FRANCE = 'FR' -LOC_FRENCH_GUIANA = 'GF' -LOC_FRENCH_POLYNESIA = 'PF' -LOC_FRENCH_SOUTHERN_TERRITORIES = 'TF' -LOC_GABON = 'GA' -LOC_GAMBIA = 'GM' -LOC_GEORGIA = 'GE' -LOC_GERMANY = 'DE' -LOC_GHANA = 'GH' -LOC_GIBRALTAR = 'GI' -LOC_GREECE = 'GR' -LOC_GREENLAND = 'GL' -LOC_GRENADA = 'GD' -LOC_GUADELOUPE = 'GP' -LOC_GUAM = 'GU' -LOC_GUATEMALA = 'GT' -LOC_GUERNSEY = 'GG' -LOC_GUINEA = 'GN' -LOC_GUINEA_BISSAU = 'GW' -LOC_GUYANA = 'GY' -LOC_HAITI = 'HT' -LOC_HEARD_ISLAND_AND_MCDONALD_ISLANDS = 'HM' -LOC_HOLY_SEE_VATICAN_CITY_STATE = 'VA' -LOC_HONDURAS = 'HN' -LOC_HONG_KONG = 'HK' -LOC_HUNGARY = 'HU' -LOC_ICELAND = 'IS' -LOC_INDIA = 'IN' -LOC_INDONESIA = 'ID' -LOC_IRAN_ISLAMIC_REPUBLIC_OF = 'IR' -LOC_IRAQ = 'IQ' -LOC_IRELAND = 'IE' -LOC_ISLE_OF_MAN = 'IM' -LOC_ISRAEL = 'IL' -LOC_ITALY = 'IT' -LOC_JAMAICA = 'JM' -LOC_JAPAN = 'JP' -LOC_JERSEY = 'JE' -LOC_JORDAN = 'JO' -LOC_KAZAKHSTAN = 'KZ' -LOC_KENYA = 'KE' -LOC_KIRIBATI = 'KI' -LOC_KOREA_DEMOCRATIC_PEOPLES_REPUBLIC_OF = 'KP' -LOC_KOREA_REPUBLIC_OF = 'KR' -LOC_KUWAIT = 'KW' -LOC_KYRGYZSTAN = 'KG' -LOC_LAO_PEOPLES_DEMOCRATIC_REPUBLIC = 'LA' -LOC_LATVIA = 'LV' -LOC_LEBANON = 'LB' -LOC_LESOTHO = 'LS' -LOC_LIBERIA = 'LR' -LOC_LIBYAN_ARAB_JAMAHIRIYA = 'LY' -LOC_LIECHTENSTEIN = 'LI' -LOC_LITHUANIA = 'LT' -LOC_LUXEMBOURG = 'LU' -LOC_MACAO = 'MO' -LOC_MACEDONIA_THE_FORMER_YUGOSLAV_REPUBLIC_OF = 'MK' -LOC_MADAGASCAR = 'MG' -LOC_MALAWI = 'MW' -LOC_MALAYSIA = 'MY' -LOC_MALDIVES = 'MV' -LOC_MALI = 'ML' -LOC_MALTA = 'MT' -LOC_MARSHALL_ISLANDS = 'MH' -LOC_MARTINIQUE = 'MQ' -LOC_MAURITANIA = 'MR' -LOC_MAURITIUS = 'MU' -LOC_MAYOTTE = 'YT' -LOC_MEXICO = 'MX' -LOC_MICRONESIA_FEDERATED_STATES_OF = 'FM' -LOC_MOLDOVA_REPUBLIC_OF = 'MD' -LOC_MONACO = 'MC' -LOC_MONGOLIA = 'MN' -LOC_MONTENEGRO = 'ME' -LOC_MONTSERRAT = 'MS' -LOC_MOROCCO = 'MA' -LOC_MOZAMBIQUE = 'MZ' -LOC_MYANMAR = 'MM' -LOC_NAMIBIA = 'NA' -LOC_NAURU = 'NR' -LOC_NEPAL = 'NP' -LOC_NETHERLANDS = 'NL' -LOC_NETHERLANDS_ANTILLES = 'AN' -LOC_NEW_CALEDONIA = 'NC' -LOC_NEW_ZEALAND = 'NZ' -LOC_NICARAGUA = 'NI' -LOC_NIGER = 'NE' -LOC_NIGERIA = 'NG' -LOC_NIUE = 'NU' -LOC_NORFOLK_ISLAND = 'NF' -LOC_NORTHERN_MARIANA_ISLANDS = 'MP' -LOC_NORWAY = 'NO' -LOC_OMAN = 'OM' -LOC_PAKISTAN = 'PK' -LOC_PALAU = 'PW' -LOC_PALESTINIAN_TERRITORY_OCCUPIED = 'PS' -LOC_PANAMA = 'PA' -LOC_PAPUA_NEW_GUINEA = 'PG' -LOC_PARAGUAY = 'PY' -LOC_PERU = 'PE' -LOC_PHILIPPINES = 'PH' -LOC_PITCAIRN = 'PN' -LOC_POLAND = 'PL' -LOC_PORTUGAL = 'PT' -LOC_PUERTO_RICO = 'PR' -LOC_QATAR = 'QA' -LOC_REUNION = 'RE' -LOC_ROMANIA = 'RO' -LOC_RUSSIAN_FEDERATION = 'RU' -LOC_RWANDA = 'RW' -LOC_SAINT_BARTHELEMY = 'BL' -LOC_SAINT_HELENA_ASCENSION_AND_TRISTAN_DA_CUNHA = 'SH' -LOC_SAINT_KITTS_AND_NEVIS = 'KN' -LOC_SAINT_LUCIA = 'LC' -LOC_SAINT_MARTIN_FRENCH_PART = 'MF' -LOC_SAINT_PIERRE_AND_MIQUELON = 'PM' -LOC_SAINT_VINCENT_AND_THE_GRENADINES = 'VC' -LOC_SAMOA = 'WS' -LOC_SAN_MARINO = 'SM' -LOC_SAO_TOME_AND_PRINCIPE = 'ST' -LOC_SAUDI_ARABIA = 'SA' -LOC_SENEGAL = 'SN' -LOC_SERBIA = 'RS' -LOC_SEYCHELLES = 'SC' -LOC_SIERRA_LEONE = 'SL' -LOC_SINGAPORE = 'SG' -LOC_SLOVAKIA = 'SK' -LOC_SLOVENIA = 'SI' -LOC_SOLOMON_ISLANDS = 'SB' -LOC_SOMALIA = 'SO' -LOC_SOUTH_AFRICA = 'ZA' -LOC_SOUTH_GEORGIA_AND_THE_SOUTH_SANDWICH_ISLANDS = 'GS' -LOC_SPAIN = 'ES' -LOC_SRI_LANKA = 'LK' -LOC_SUDAN = 'SD' -LOC_SURINAME = 'SR' -LOC_SVALBARD_AND_JAN_MAYEN = 'SJ' -LOC_SWAZILAND = 'SZ' -LOC_SWEDEN = 'SE' -LOC_SWITZERLAND = 'CH' -LOC_SYRIAN_ARAB_REPUBLIC = 'SY' -LOC_TAIWAN_PROVINCE_OF_CHINA = 'TW' -LOC_TAJIKISTAN = 'TJ' -LOC_TANZANIA_UNITED_REPUBLIC_OF = 'TZ' -LOC_THAILAND = 'TH' -LOC_TIMOR_LESTE = 'TL' -LOC_TOGO = 'TG' -LOC_TOKELAU = 'TK' -LOC_TONGA = 'TO' -LOC_TRINIDAD_AND_TOBAGO = 'TT' -LOC_TUNISIA = 'TN' -LOC_TURKEY = 'TR' -LOC_TURKMENISTAN = 'TM' -LOC_TURKS_AND_CAICOS_ISLANDS = 'TC' -LOC_TUVALU = 'TV' -LOC_UGANDA = 'UG' -LOC_UKRAINE = 'UA' -LOC_UNITED_ARAB_EMIRATES = 'AE' -LOC_UNITED_KINGDOM = 'GB' -LOC_UNITED_STATES = 'US' -LOC_UNITED_STATES_MINOR_OUTLYING_ISLANDS = 'UM' -LOC_URUGUAY = 'UY' -LOC_UZBEKISTAN = 'UZ' -LOC_VANUATU = 'VU' -LOC_VENEZUELA_BOLIVARIAN_REPUBLIC_OF = 'VE' -LOC_VIET_NAM = 'VN' -LOC_VIRGIN_ISLANDS_BRITISH = 'VG' -LOC_VIRGIN_ISLANDS_US = 'VI' -LOC_WALLIS_AND_FUTUNA = 'WF' -LOC_WESTERN_SAHARA = 'EH' -LOC_YEMEN = 'YE' -LOC_ZAMBIA = 'ZM' -LOC_ZIMBABWE = 'ZW' diff --git a/decorator.py b/decorator.py deleted file mode 100644 index 57cb1a6..0000000 --- a/decorator.py +++ /dev/null @@ -1,254 +0,0 @@ -########################## LICENCE ############################### - -# Copyright (c) 2005-2012, Michele Simionato -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: - -# Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# Redistributions in bytecode form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in -# the documentation and/or other materials provided with the -# distribution. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS -# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -# DAMAGE. - -""" -Decorator module, see http://pypi.python.org/pypi/decorator -for the documentation. -""" - -__version__ = '3.4.0' - -__all__ = ["decorator", "FunctionMaker", "contextmanager"] - -import sys, re, inspect -if sys.version >= '3': - from inspect import getfullargspec - def get_init(cls): - return cls.__init__ -else: - class getfullargspec(object): - "A quick and dirty replacement for getfullargspec for Python 2.X" - def __init__(self, f): - self.args, self.varargs, self.varkw, self.defaults = \ - inspect.getargspec(f) - self.kwonlyargs = [] - self.kwonlydefaults = None - def __iter__(self): - yield self.args - yield self.varargs - yield self.varkw - yield self.defaults - def get_init(cls): - return cls.__init__.__func__ - -DEF = re.compile('\s*def\s*([_\w][_\w\d]*)\s*\(') - -# basic functionality -class FunctionMaker(object): - """ - An object with the ability to create functions with a given signature. - It has attributes name, doc, module, signature, defaults, dict and - methods update and make. - """ - def __init__(self, func=None, name=None, signature=None, - defaults=None, doc=None, module=None, funcdict=None): - self.shortsignature = signature - if func: - # func can be a class or a callable, but not an instance method - self.name = func.__name__ - if self.name == '': # small hack for lambda functions - self.name = '_lambda_' - self.doc = func.__doc__ - self.module = func.__module__ - if inspect.isfunction(func): - argspec = getfullargspec(func) - self.annotations = getattr(func, '__annotations__', {}) - for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', - 'kwonlydefaults'): - setattr(self, a, getattr(argspec, a)) - for i, arg in enumerate(self.args): - setattr(self, 'arg%d' % i, arg) - if sys.version < '3': # easy way - self.shortsignature = self.signature = \ - inspect.formatargspec( - formatvalue=lambda val: "", *argspec)[1:-1] - else: # Python 3 way - allargs = list(self.args) - allshortargs = list(self.args) - if self.varargs: - allargs.append('*' + self.varargs) - allshortargs.append('*' + self.varargs) - elif self.kwonlyargs: - allargs.append('*') # single star syntax - for a in self.kwonlyargs: - allargs.append('%s=None' % a) - allshortargs.append('%s=%s' % (a, a)) - if self.varkw: - allargs.append('**' + self.varkw) - allshortargs.append('**' + self.varkw) - self.signature = ', '.join(allargs) - self.shortsignature = ', '.join(allshortargs) - self.dict = func.__dict__.copy() - # func=None happens when decorating a caller - if name: - self.name = name - if signature is not None: - self.signature = signature - if defaults: - self.defaults = defaults - if doc: - self.doc = doc - if module: - self.module = module - if funcdict: - self.dict = funcdict - # check existence required attributes - assert hasattr(self, 'name') - if not hasattr(self, 'signature'): - raise TypeError('You are decorating a non function: %s' % func) - - def update(self, func, **kw): - "Update the signature of func with the data in self" - func.__name__ = self.name - func.__doc__ = getattr(self, 'doc', None) - func.__dict__ = getattr(self, 'dict', {}) - func.__defaults__ = getattr(self, 'defaults', ()) - func.__kwdefaults__ = getattr(self, 'kwonlydefaults', None) - func.__annotations__ = getattr(self, 'annotations', None) - callermodule = sys._getframe(3).f_globals.get('__name__', '?') - func.__module__ = getattr(self, 'module', callermodule) - func.__dict__.update(kw) - - def make(self, src_templ, evaldict=None, addsource=False, **attrs): - "Make a new function from a given template and update the signature" - src = src_templ % vars(self) # expand name and signature - evaldict = evaldict or {} - mo = DEF.match(src) - if mo is None: - raise SyntaxError('not a valid function template\n%s' % src) - name = mo.group(1) # extract the function name - names = set([name] + [arg.strip(' *') for arg in - self.shortsignature.split(',')]) - for n in names: - if n in ('_func_', '_call_'): - raise NameError('%s is overridden in\n%s' % (n, src)) - if not src.endswith('\n'): # add a newline just for safety - src += '\n' # this is needed in old versions of Python - try: - code = compile(src, '', 'single') - # print >> sys.stderr, 'Compiling %s' % src - exec(code, evaldict) - except: - #print('Error in generated code:', file=sys.stderr) # Gives error in 2.7 - #print(src, file=sys.stderr) - print('Error in generated code:') - print(src) - - raise - func = evaldict[name] - if addsource: - attrs['__source__'] = src - self.update(func, **attrs) - return func - - @classmethod - def create(cls, obj, body, evaldict, defaults=None, - doc=None, module=None, addsource=True, **attrs): - """ - Create a function from the strings name, signature and body. - evaldict is the evaluation dictionary. If addsource is true an attribute - __source__ is added to the result. The attributes attrs are added, - if any. - """ - if isinstance(obj, str): # "name(signature)" - name, rest = obj.strip().split('(', 1) - signature = rest[:-1] #strip a right parens - func = None - else: # a function - name = None - signature = None - func = obj - self = cls(func, name, signature, defaults, doc, module) - ibody = '\n'.join(' ' + line for line in body.splitlines()) - return self.make('def %(name)s(%(signature)s):\n' + ibody, - evaldict, addsource, **attrs) - -def decorator(caller, func=None): - """ - decorator(caller) converts a caller function into a decorator; - decorator(caller, func) decorates a function using a caller. - """ - if func is not None: # returns a decorated function - evaldict = func.__globals__.copy() - evaldict['_call_'] = caller - evaldict['_func_'] = func - return FunctionMaker.create( - func, "return _call_(_func_, %(shortsignature)s)", - evaldict, undecorated=func, __wrapped__=func) - else: # returns a decorator - if inspect.isclass(caller): - name = caller.__name__.lower() - callerfunc = get_init(caller) - doc = 'decorator(%s) converts functions/generators into ' \ - 'factories of %s objects' % (caller.__name__, caller.__name__) - fun = getfullargspec(callerfunc).args[1] # second arg - elif inspect.isfunction(caller): - name = '_lambda_' if caller.__name__ == '' \ - else caller.__name__ - callerfunc = caller - doc = caller.__doc__ - fun = getfullargspec(callerfunc).args[0] # first arg - else: # assume caller is an object with a __call__ method - name = caller.__class__.__name__.lower() - callerfunc = caller.__call__.__func__ - doc = caller.__call__.__doc__ - fun = getfullargspec(callerfunc).args[1] # second arg - evaldict = callerfunc.__globals__.copy() - evaldict['_call_'] = caller - evaldict['decorator'] = decorator - return FunctionMaker.create( - '%s(%s)' % (name, fun), - 'return decorator(_call_, %s)' % fun, - evaldict, undecorated=caller, __wrapped__=caller, - doc=doc, module=caller.__module__) - -######################### contextmanager ######################## - -def __call__(self, func): - 'Context manager decorator' - return FunctionMaker.create( - func, "with _self_: return _func_(%(shortsignature)s)", - dict(_self_=self, _func_=func), __wrapped__=func) - -try: # Python >= 3.2 - - from contextlib import _GeneratorContextManager - ContextManager = type( - 'ContextManager', (_GeneratorContextManager,), dict(__call__=__call__)) - -except ImportError: # Python >= 2.5 - - from contextlib import GeneratorContextManager - def __init__(self, f, *a, **k): - return GeneratorContextManager.__init__(self, f(*a, **k)) - ContextManager = type( - 'ContextManager', (GeneratorContextManager,), - dict(__call__=__call__, __init__=__init__)) - -contextmanager = decorator(ContextManager) diff --git a/iRWebStats.py b/iRWebStats.py deleted file mode 100644 index 3bd4fd3..0000000 --- a/iRWebStats.py +++ /dev/null @@ -1,719 +0,0 @@ -#!/usr/bin/env python -""" iRWebStats class. Check examples.py for example usage. """ -__author__ = "Jeyson Molina" -__email__ = "jjmc82@gmail.com" -__version__ = "1.0" - - -import urllib - -try: - import urllib.parse - encode = urllib.parse.urlencode # python3 -except: - encode = urllib.urlencode # python2 - -import requests -import constants as ct -import datetime -import csv -import time -import json -import re -import io -from decimal import Decimal -from util import * - -requests.packages.urllib3.disable_warnings() - -class iRWebStats: - global custid - """ Use this class to connect to iRacing website and request some stats - from drivers, races and series. It needs to be logged in the - iRacing membersite so valid login crendentials (user, password) - are required. Most data is returned in JSON format and - converted to python dicts. """ - - def __init__(self, verbose=True): - self.last_cookie = '' - self.logged = False - self.custid = 0 - self.verbose = verbose - self.TRACKS, self.CARS, self.DIVISION, self.CARCLASS, self.CLUB = {},\ - {}, {}, {}, {} - - def __save_cookie(self): - """ Saves the current cookie to disk from a successful login to avoid - future login procedures and save time. A cookie usually last - at least a couple of hours """ - - #print("Saving cookie for future use") - o = open('cookie.tmp', 'w') - o.write(self.last_cookie) - o.write('\r\n' + str(self.custid)) - o.close() - - def __load_cookie(self): - """ Loads a previously saved cookie """ - - try: - o = open('cookie.tmp', 'r') - self.last_cookie, self.custid = o.read().split('\r\n') - o.close() - return True - except: - return False - - def login(self, username='', password='', quiet=False): - """ Log in to iRacing members site. If there is a valid cookie saved - then it tries to use it to avoid a new login request. Returns - True is the login was succesful and stores the customer id - (custid) of the current login in self.custid. """ - - if self.logged: - return True - data = {"username": username, "password": password, 'utcoffset': 300, - 'todaysdate': ''} - try: - if not quiet: - print("Loggin in...") - # Check if there's a previous cookie - if (self.__load_cookie() and self.__check_cookie()): - # If previous cookie is valid - if not quiet: - print("Previous cookie valid") - self.logged = True - # Load iracing info - if not quiet: - self.__get_irservice_info(self.__req(ct.URL_IRACING_HOME, - cookie=self.last_cookie)) - # TODO Should we cache this? - return self.logged - self.custid = '' - r = self.__req(ct.URL_IRACING_LOGIN, grab_cookie=True) - if r.find("

iRacing Is Offline

") > -1: - print("iRacing is Offline, try again later") - self.logged = False - else: - r = self.__req(ct.URL_IRACING_LOGIN2, data, - cookie=self.last_cookie, grab_cookie=True) - - if 'irsso_members' in self.last_cookie: - ind = r.index('js_custid') - custid = int(r[ind + 11: r.index(';', ind)]) - self.custid = custid - self.logged = True - if not quiet: - self.__get_irservice_info(r) - self.__save_cookie() - if not quiet: - print("Log in succesful") - else: - if not quiet: - print("Invalid Login (user: %s).\n\rPlease check your credentials" % (username)) - self.logged = False - - except Exception as e: - self.logged = False - return self.logged - - def logout(self): - self.logged = False # TODO proper logout - - def __check_cookie(self): - """ Checks the cookie by testing a request response""" - - r = parse(self.__req(ct.URL_DRIVER_COUNTS, cookie=self.last_cookie)) - if isinstance(r, dict): - return True - return False - - def __req(self, url, data=None, cookie=None, grab_cookie=False, - useget=False): - """ Creates and sends the HTTP requests to iRacing site """ - - # Sleep/wait to avoid flooding the service with requests - time.sleep(ct.WAIT_TIME) # 0.3 seconds - h = ct.HEADERS.copy() - if cookie is not None: # Send the cookie - h['Cookie'] = cookie - elif len(self.last_cookie): - h['Cookie'] = self.last_cookie - - if (data is None) or useget: - try: - req = requests.get(url, headers=h, params=data, verify=False) - except requests.exceptions.ConnectionError as e: - print("Unable to connect to iracing.com") - except (requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout) as e: - print("Timeout connecting to iracing.com") - - else: - h['Content-Type'] = 'application/x-www-form-urlencoded;\ - charset=UTF-8' - req = requests.post(url, data=data, headers=h, verify=False) - if 'Set-Cookie' in req.headers and grab_cookie: - self.last_cookie = req.headers['Set-Cookie'] - html = req.text - return html - - def __get_irservice_info(self, resp): - """ Gets general information from iracing service like current tracks, - cars, series, etc. Check self.TRACKS, self.CARS, self.DIVISION - , self.CARCLASS, self.CLUB. """ - - print("Getting iRacing Service info (cars, tracks, etc.)") - items = {"TRACKS": "TrackListing", "CARS": "CarListing", - "CARCLASS": "CarClassListing", "CLUBS": "ClubListing", - "SEASON": "SeasonListing", "DIVISION": "DivisionListing", - "YEARANDQUARTER": "YearAndQuarterListing"} - for i in items: - str2find = "var " + items[i] + " = extractJSON('" - try: - ind1 = resp.index(str2find) - json_o = resp[ind1 + len(str2find): resp.index("');", ind1)]\ - .replace('+', ' ') - o = json.loads(json_o) - if i not in ("SEASON", "YEARANDQUARTER"): - o = {ele['id']: ele for ele in o} - setattr(self, i, o) # i.e self.TRACKS = o - - except Exception as e: - print(("Error ocurred. Couldn't get %s" % i)) - - - @logged_in - def iratingchart(self, custid=None, category=ct.IRATING_ROAD_CHART): - """ Gets the irating data of a driver using its custom id (custid) - that generates the chart located in the driver's profile. """ - - r = self.__req(ct.URL_STATS_CHART % (custid, category), - cookie=self.last_cookie) - return parse(r) - - @logged_in - def driver_counts(self): - """ Gets list of connected myracers and notifications. """ - - r = self.__req(ct.URL_DRIVER_COUNTS, cookie=self.last_cookie) - return parse(r) - - @logged_in - def career_stats(self, custid=None): - """ Gets career stats (top5, top 10, etc.) of driver (custid).""" - - r = self.__req(ct.URL_CAREER_STATS % (custid), - cookie=self.last_cookie) - return parse(r)[0] - - @logged_in - def yearly_stats(self, custid=None): - """ Gets yearly stats (top5, top 10, etc.) of driver (custid).""" - - r = self.__req(ct.URL_YEARLY_STATS % (custid), - cookie=self.last_cookie) - # tofile(r) - return parse(r) - - @logged_in - def cars_driven(self, custid=None): - """ Gets list of cars driven by driver (custid).""" - - r = self.__req(ct.URL_CARS_DRIVEN % (custid), - cookie=self.last_cookie) - # tofile(r) - return parse(r) - - @logged_in - def personal_best(self, custid=None, carid=0): - """ Personal best times of driver (custid) using car - (carid. check self.CARS) set in official events.""" - - r = self.__req(ct.URL_PERSONAL_BEST % (carid, custid), - cookie=self.last_cookie) - return parse(r) - - @logged_in - def driverdata(self, drivername): - """ Personal data of driver using its name in the request - (i.e drivername="Victor Beltran"). """ - - r = self.__req(ct.URL_DRIVER_STATUS % (encode({ - 'searchTerms': drivername})), cookie=self.last_cookie) - # tofile(r) - return parse(r) - - @logged_in - def lastrace_stats(self, custid=None): - """ Gets stats of last races (10 max?) of driver (custid).""" - - r = self.__req(ct.URL_LASTRACE_STATS % (custid), - cookie=self.last_cookie) - return parse(r) - - @logged_in - def driver_search(self, race_type=ct.RACE_TYPE_ROAD, location=ct.LOC_ALL, - license=(ct.LIC_ROOKIE, ct.ALL), irating=(0, ct.ALL), - ttrating=(0, ct.ALL), avg_start=(0, ct.ALL), - avg_finish=(0, ct.ALL), avg_points=(0, ct.ALL), - avg_incs=(0, ct.ALL), active=False, - sort=ct.SORT_IRATING, page=1, order=ct.ORDER_DESC): - """Search drivers using several search fields. A tuple represent a - range (i.e irating=(1000, 2000) gets drivers with irating - between 1000 and 2000). Use ct.ALL used in the lower or - upperbound of a range disables that limit. Returns a tuple - (results, total_results) so if you want all results you should - request different pages (using page) until you gather all - total_results. Each page has 25 (ct.NUM_ENTRIES) results max.""" - - lowerbound = ct.NUM_ENTRIES * (page - 1) + 1 - upperbound = lowerbound + ct.NUM_ENTRIES - 1 - search = 'null' - friend = ct.ALL # TODO - studied = ct.ALL # TODO - recent = ct.ALL # TODO - - active = int(active) - # Data to POST - data = {'custid': self.custid, 'search': search, 'friend': friend, - 'watched': studied, 'country': location, 'recent': recent, - 'category': race_type, 'classlow': license[0], - 'classhigh': license[1], 'iratinglow': irating[0], - 'iratinghigh': irating[1], 'ttratinglow': ttrating[0], - 'ttratinghigh': ttrating[1], 'avgstartlow': avg_start[0], - 'avgstarthigh': avg_start[1], 'avgfinishlow': avg_finish[0], - 'avgfinishhigh': avg_finish[1], 'avgpointslow': avg_points[0], - 'avgpointshigh': avg_points[1], 'avgincidentslow': - avg_incs[0], 'avgincidentshigh': avg_incs[1], - 'lowerbound': lowerbound, 'upperbound': upperbound, - 'sort': sort, 'order': order, 'active': active} - - total_results, drivers = 0, {} - - try: - r = self.__req(ct.URL_DRIVER_STATS, data=data, - cookie=self.last_cookie) - res = parse(r) - total_results = res['d']['33'] - - header = res['m'] - f = res['d']['r'][0] - if int(f['29']) == int(self.custid): # 29 is custid - drivers = res['d']['r'][1:] - else: - drivers = res['d']['r'] - drivers = format_results(drivers, header) - - except Exception as e: - print("Error fetching driver search data. Error: %s" % e) - - return drivers, total_results - - def test(self, a, b=2, c=3): - return a, b, c - - @logged_in - def results_archive(self, custid=None, race_type=ct.RACE_TYPE_ROAD, - event_types=ct.ALL, official=ct.ALL, - license_level=ct.ALL, car=ct.ALL, track=ct.ALL, - series=ct.ALL, season=(2014, 2, ct.ALL), - date_range=ct.ALL, page=1, sort=ct.SORT_TIME, - order= ct.ORDER_DESC): - """ Search race results using various fields. Returns a tuple - (results, total_results) so if you want all results you should - request different pages (using page). Each page has 25 - (ct.NUM_ENTRIES) results max.""" - - format_ = 'json' - lowerbound = ct.NUM_ENTRIES * (page - 1) + 1 - upperbound = lowerbound + ct.NUM_ENTRIES - 1 - # TODO carclassid, seriesid in constants - data = {'format': format_, 'custid': custid, 'seriesid': series, - 'carid': car, 'trackid': track, 'lowerbound': lowerbound, - 'upperbound': upperbound, 'sort': sort, 'order': order, - 'category': race_type, 'showtts': 0, 'showraces': 0, - 'showweek': 0, 'showops': 0, 'showofficial': 0, - 'showunofficial': 0, 'showrookie': 0, 'showclassa': 0, - 'showclassb': 0, 'showclassc': 0, 'showclassd': 0, - 'showpro': 0, 'showprowc': 0, } - # Events - ev_vars = {ct.EVENT_RACE: 'showraces', ct.EVENT_QUALY: 'showquals', - ct.EVENT_PRACTICE: 'showops', ct.EVENT_TTRIAL: 'showtts'} - if event_types == ct.ALL: - event_types = (ct.EVENT_RACE, ct.EVENT_QUALY, ct.EVENT_PRACTICE, - ct.EVENT_TTRIAL) - - for v in event_types: - data[ev_vars[v]] = 1 - # Official, unofficial - if official == ct.ALL: - data['showofficial'] = 1 - data['showunoofficial'] = 1 - else: - if ct.EVENT_UNOFFICIAL in official: - data['showunofficial'] = 1 - if ct.EVENT_OFFICIAL in official: - data['showofficial'] = 1 - - # Season - if date_range == ct.ALL: - data['seasonyear'] = season[0] - data['seasonquarter'] = season[1] - if season[2] != ct.ALL: - data['raceweek'] = season[2] - else: - # Date range - tc = lambda s:\ - time.mktime(datetime.datetime.strptime(s, "%Y-%m-%d").timetuple()) * 1000 - data['starttime_low'] = Decimal(tc(date_range[0])) # multiplied by 1000 | Fuzz wrapped this in Decimal to get it working in python 2.7 - data['starttime_high'] = Decimal(tc(date_range[1])) # Fuzz wrapped this in Decimal to get it working in python 2.7 - - # License levels - lic_vars = {ct.LIC_ROOKIE: 'showrookie', ct.LIC_A: 'showclassa', - ct.LIC_B: 'showclassb', ct.LIC_C: 'showclassc', - ct.LIC_D: 'showclassd', ct.LIC_PRO: 'showpro', - ct.LIC_PRO_WC: 'showprowc'} - - if license_level == ct.ALL: - license_level = (ct.LIC_ROOKIE, ct.LIC_A, ct.LIC_B, ct.LIC_C, - ct.LIC_D, ct.LIC_PRO, ct.LIC_PRO_WC) - for v in license_level: - data[lic_vars[v]] = 1 - - r = self.__req(ct.URL_RESULTS_ARCHIVE, data=data, - cookie=self.last_cookie) - res = parse(r) - total_results, results = 0, [] - if len(res['d']): - total_results = res['d']['46'] - results = res['d']['r'] - header = res['m'] - results = format_results(results, header) - - return results, total_results - - @logged_in - def season_standings(self, season, carclass, club=ct.ALL, raceweek=ct.ALL, - division=ct.ALL, sort=ct.SORT_POINTS, - order=ct.ORDER_DESC, page=1): - """ Search season standings using various fields. season, carclass - and club are ids. Returns a tuple (results, total_results) so - if you want all results you should request different pages - (using page) until you gather all total_results. Each page has - 25 results max.""" - - lowerbound = ct.NUM_ENTRIES * (page - 1) + 1 - upperbound = lowerbound + ct.NUM_ENTRIES - 1 - - data = {'sort': sort, 'order': order, 'seasonid': season, - 'carclassid': carclass, 'clubid': club, 'raceweek': raceweek, - 'division': division, 'start': lowerbound, 'end': upperbound} - r = self.__req(ct.URL_SEASON_STANDINGS, data=data) - res = parse(r) - total_results = res['d']['27'] - results = res['d']['r'] - header = res['m'] - results = format_results(results, header) - - return results, total_results - - @logged_in - def season_tt_standings(self, season, carclass, club=ct.ALL, raceweek=ct.ALL, - division=ct.ALL, sort=ct.SORT_POINTS, - order=ct.ORDER_DESC, page=1): - """ Search season Time Trial standings using various fields. season, carclass - and club are ids. Returns a tuple (results, total_results) so - if you want all results you should request different pages - (using page) until you gather all total_results. Each page has - 25 results max.""" - - lowerbound = ct.NUM_ENTRIES * (page - 1) + 1 - upperbound = lowerbound + ct.NUM_ENTRIES - 1 - - data = {'sort': sort, 'order': order, 'seasonid': season, - 'carclassid': carclass, 'clubid': club, 'raceweek': raceweek, - 'division': division, 'start': lowerbound, 'end': upperbound} - r = self.__req(ct.URL_SEASON_TT_STANDINGS, data=data) - res = parse(r) - total_results = res['d']['27'] - results = res['d']['r'] - header = res['m'] - results = format_results(results, header) - - return results, total_results - - @logged_in - def season_tt_record(self, season, carclass, raceweek): - """ Find the fastest time trial time for a seasonid and week num.""" - - raceweek += 1 - data = {'seasonid': season, 'carclassid': carclass, 'raceweek': raceweek, 'start': 0, 'end': 1} - r = self.__req(ct.URL_SEASON_TT_RES, data=data) - res = parse(r) - try: - results = res['d']['r'] - header = res['m'] - results = format_results(results, header) - record = results[0][u'besttime'] - - return record - except: - return None - - @logged_in - def season_qual_results(self, season, carclass, club=ct.ALL, raceweek=ct.ALL, - division=ct.ALL, sort=ct.SORT_POINTS, - order=ct.ORDER_DESC, page=1): - """ Search season Qualifying standings using various fields. season, carclass - and club are ids. Returns a tuple (results, total_results) so - if you want all results you should request different pages - (using page) until you gather all total_results. Each page has - 25 results max.""" - - lowerbound = ct.NUM_ENTRIES * (page - 1) + 1 - upperbound = lowerbound + ct.NUM_ENTRIES - 1 - - data = {'sort': sort, 'order': order, 'seasonid': season, - 'carclassid': carclass, 'clubid': club, 'raceweek': raceweek, - 'division': division, 'start': lowerbound, 'end': upperbound} - r = self.__req(ct.URL_SEASON_QUAL_RES, data=data) - res = parse(r) - print(res) - total_results = res['d']['27'] - results = res['d']['r'] - header = res['m'] - results = format_results(results, header) - - return results, total_results - - @logged_in - def season_qual_pole(self, season, carclass, raceweek): - """ Find the fastest qualifying time for a seasonid and week num.""" - - raceweek += 1 - data = {'seasonid': season, 'carclassid': carclass, 'raceweek': raceweek, 'start': 0, 'end': 1} - r = self.__req(ct.URL_SEASON_QUAL_RES, data=data) - res = parse(r) - try: - results = res['d']['r'] - header = res['m'] - results = format_results(results, header) - pole = results[0][u'bestqualtime'] - - return pole - except: - return None - - def last_series(self, userid): - """ Returns stats for the last 3 series the driver has raced in """ - - r = self.__req(ct.URL_LAST_SERIES % userid) - res = parse(r) - - return res - - @logged_in - def my_season_standings(self, season, start=1, end=1): - """ Returns the logged in user's current standing in a series, along - with the total number of drivers """ - - r = self.__req(ct.URL_AGGREGATE_SEASON_STANDINGS % (season, start, end)) - res = parse(r) - - try: - standing = res['custrow'] - total_drivers = res['rowcount'] - except: - standing = 0 - total_drivers = 0 - - return standing, total_drivers - - @logged_in - def hosted_results(self, session_host=None, session_name=None, - date_range=None, sort=ct .SORT_TIME, - order=ct.ORDER_DESC, page=1): - """ Search hosted races results using various fields. Returns a tuple - (results, total_results) so if you want all results you should - request different pages (using page) until you gather all - total_results. Each page has 25 (ct.NUM_ENTRIES) results max.""" - - lowerbound = ct.NUM_ENTRIES * (page - 1) + 1 - upperbound = lowerbound + ct.NUM_ENTRIES - 1 - - data = {'sort': sort, 'order': order, 'lowerbound': lowerbound, - 'upperbound': upperbound} - if session_host is not None: - data['sessionhost'] = session_host - if session_name is not None: - data['sessionname'] = session_name - - if date_range is not None: - # Date range - tc = lambda s:\ - time.mktime(datetime.datetime.strptime(s, "%Y-%m-%d"). - timetuple()) * 1000 - data['starttime_lowerbound'] = tc(date_range[0]) - # multiplied by 1000 - data['starttime_upperbound'] = tc(date_range[1]) - - r = self.__req(ct.URL_HOSTED_RESULTS, data=data) - # tofile(r) - res = parse(r) - total_results = res['rowcount'] - results = res['rows'] # doesn't need format_results - return results, total_results - - @logged_in - def session_times(self, series_season, start, end): - """ Gets Current and future sessions (qualy, practice, race) - of series_season """ - - r = self.__req(ct.URL_SESSION_TIMES, data={'start': start, 'end': end, - 'season': series_season}, useget=True) - return parse(r) - - @logged_in - def series_raceresults(self, season, raceweek): - """ Gets races results of all races of season in specified raceweek """ - - r = self.__req(ct.URL_SERIES_RACERESULTS, data={'seasonid': season, - 'raceweek': raceweek}) # TODO no bounds? - res = parse(r) - header = res['m'] - results = res['d'] - results = format_results(results, header) - return results - - @logged_in - def event_results(self, subsession, sessnum=0): - """ Gets the event results (table of positions, times, etc.). The - event is identified by a subsession id. """ - r = self.__req(ct.URL_GET_EVENTRESULTS % (subsession, sessnum))\ - .encode('utf8') - data = [x for x in csv.reader(io.StringIO(r), delimiter=',', - quotechar='"')] - header_ev, header_res = data[0], data[3] - event_info = dict(list(zip(header_ev, data[1]))) - results = [dict(list(zip(header_res, x))) for x in data[4:]] - - return event_info, results - - @logged_in - def event_results2(self, subsession, custid): - """ Get the event results from the web page rather than CSV. - Required to get ttRating for time trials """ - - r = self.__req(ct.URL_GET_EVENTRESULTS2 % (subsession, custid), - cookie=self.last_cookie) - - resp = re.sub('\t+',' ',r) - resp = re.sub('\r\r\n+',' ',resp) - resp = re.sub('\s+',' ',resp) - - str2find = "var resultOBJ =" - ind1 = resp.index(str2find) - ind2 = resp.index("};", ind1) + 1 - resp = resp[ind1 + len(str2find): ind2].replace('+', ' ') - - ttitems = ("custid", "isOfficial", "carID", "avglaptime", "fastestlaptime", "fastestlaptimems", "fastestlapnum", "bestnlapstime", "bestnlapsnum", "lapscomplete", "incidents", "newttRating", "oldttRating", "sr_new", "sr_old", "reasonOutName") - out = "" - for ttitem in ttitems: - ind1 = resp.index(ttitem) - ind2 = resp.index(",", ind1) + 1 - out = out + resp[ind1: ind2] - - out = re.sub(r"{\s*(\w)", r'{"\1', out) - out = re.sub(r",\s*(\w)", r',"\1', out) - out = re.sub(r"(\w):", r'\1":', out) - out = re.sub(r":\"(\d)\":", r':"\1:', out) - out = re.sub(r"parseFloat\((\"\d\.\d\d\")\)", r'\1', out) - - out = out.strip().rstrip(',') - out = "{\"" + out + "}" - out = json.loads(out) - - return out - - @logged_in - def past_series(self): - """ Get a list of all past series """ - - r = self.__req(ct.URL_GET_PASTSERIES, - cookie=self.last_cookie) - - start = r.find("var series") - end = r.find("expandcollapseseries") - resp = r[start:end] - resp = re.sub('\t+',' ',resp) - resp = re.sub('\r\r\n+',' ',resp) - resp = re.sub('\s+',' ',resp) - - out = dict() - pattern = "id:([0-9]*), name:\"([^\"]*)\", header_img" - regex = re.compile(pattern, re.IGNORECASE) - for match in regex.finditer(resp): - out[match.group(1)] = match.group(2) - - return out - - def subsession_results(self, subsession, custid): - """ Get the results for a time trial event from the web page. - """ - - r = self.__req(ct.URL_GET_SUBSESSRESULTS % (subsession, custid), useget=True) - - out = parse(r)['rows'] - - return out - - def event_laps_single(self, subsession, custid, sessnum=0): - """ Get the lap times for an event from the web page. - """ - - r = self.__req(ct.URL_GET_LAPS_SINGLE % (subsession, custid, sessnum), - cookie=self.last_cookie) - - out = parse(r) - - return out - - def event_laps_all(self, subsession): - """ Get the lap times for an event from the web page. - """ - - r = self.__req(ct.URL_GET_LAPS_ALL % subsession, - cookie=self.last_cookie) - - out = parse(r) - - return out - - def best_lap(self, subsessionid, custid): - """ Get the best lap time for a driver from an event. - """ - - laptime = self.event_laps_single(subsessionid, custid)['drivers'][0]['bestlaptime'] - - return laptime - - def world_record(self, seasonyear, seasonquarter, carid, trackid, custid): - """ Get the world record lap time for certain car in a season. - """ - - r = self.__req(ct.URL_GET_WORLDRECORD % (seasonyear, seasonquarter, carid, trackid, custid), - cookie=self.last_cookie) - out = parse(r) - - subsessionid = out["d"]["r"][1]["17"] - record_holder = out["d"]["r"][1]["26"] - - record = self.best_lap(subsessionid, record_holder) - - return record - -if __name__ == '__main__': - irw = iRWebStats() - user, passw = ('username', 'password') - irw.login(user, passw) - print("Cars Driven", irw.cars_driven()) # example usage diff --git a/ir_webstats b/ir_webstats new file mode 160000 index 0000000..392d69c --- /dev/null +++ b/ir_webstats @@ -0,0 +1 @@ +Subproject commit 392d69c4241ec0283d7c7584e87b5fead6404b09 diff --git a/irsdk.py b/irsdk.py deleted file mode 100644 index 20ef151..0000000 --- a/irsdk.py +++ /dev/null @@ -1,545 +0,0 @@ -#!python3 - -import re -import argparse -import mmap -import struct -import ctypes -import yaml -from urllib import request -from yaml.reader import Reader as YamlReader - -try: - from yaml.cyaml import CLoader as YamlLoader -except ImportError: - from yaml import Loader as YamlLoader - -VERSION = '1.1.2' - -SIM_STATUS_URL = 'http://127.0.0.1:32034/get_sim_status?object=simStatus' - -MEMMAPFILE = 'Local\\IRSDKMemMapFileName' -MEMMAPFILESIZE = 780 * 1024 -BROADCASTMSGNAME = 'IRSDK_BROADCASTMSG' - -VAR_TYPE_MAP = ['c', '?', 'i', 'I', 'f', 'd'] - -class StatusField: - status_connected = 1 - -class EngineWarnings: - water_temp_warning = 0x01 - fuel_pressure_warning = 0x02 - oil_pressure_warning = 0x04 - engine_stalled = 0x08 - pit_speed_limiter = 0x10 - rev_limiter_active = 0x20 - -class Flags: - # global flags - checkered = 0x00000001 - white = 0x00000002 - green = 0x00000004 - yellow = 0x00000008 - red = 0x00000010 - blue = 0x00000020 - debris = 0x00000040 - crossed = 0x00000080 - yellow_waving = 0x00000100 - one_lap_to_green = 0x00000200 - green_held = 0x00000400 - ten_to_go = 0x00000800 - five_to_go = 0x00001000 - random_waving = 0x00002000 - caution = 0x00004000 - caution_waving = 0x00008000 - - # drivers black flags - black = 0x00010000 - disqualify = 0x00020000 - servicible = 0x00040000 # car is allowed service (not a flag) - furled = 0x00080000 - repair = 0x00100000 - - # start lights - start_hidden = 0x10000000 - start_ready = 0x20000000 - start_set = 0x40000000 - start_go = 0x80000000 - -class TrkLoc: - not_in_world = -1 - off_track = 0 - in_pit_stall = 1 - aproaching_pits = 2 - on_track = 3 - -class SessionState: - invalid = 0 - get_in_car = 1 - warmup = 2 - parade_laps = 3 - racing = 4 - checkered = 5 - cool_down = 6 - -class CameraState: - is_session_screen = 0x0001 # the camera tool can only be activated if viewing the session screen (out of car) - is_scenic_active = 0x0002 # the scenic camera is active (no focus car) - - # these can be changed with a broadcast message - cam_tool_active = 0x0004 - ui_hidden = 0x0008 - use_auto_shot_selection = 0x0010 - use_temporary_edits = 0x0020 - use_key_acceleration = 0x0040 - use_key10x_acceleration = 0x0080 - use_mouse_aim_mode = 0x0100 - -class BroadcastMsg: - cam_switch_pos = 0 # car position, group, camera - cam_switch_num = 1 # driver #, group, camera - cam_set_state = 2 # CameraState, unused, unused - replay_set_play_speed = 3 # speed, slowMotion, unused - replay_set_play_position = 4 # RpyPosMode, Frame Number (high, low) - replay_search = 5 # RpySrchMode, unused, unused - replay_set_state = 6 # RpyStateMode, unused, unused - reload_textures = 7 # ReloadTexturesMode, carIdx, unused - chat_command = 8 # ChatCommandMode, subCommand, unused - pit_command = 9 # PitCommandMode, parameter - telem_command = 10# irsdk_TelemCommandMode, unused, unused - -class ChatCommandMode: - macro = 0 # pass in a number from 1-15 representing the chat macro to launch - begin_chat = 1 # Open up a new chat window - reply = 2 # Reply to last private chat - cancel = 3 # Close chat window - -class PitCommandMode: # this only works when the driver is in the car - clear = 0 # Clear all pit checkboxes - ws = 1 # Clean the winshield, using one tear off - fuel = 2 # Add fuel, optionally specify the amount to add in liters or pass '0' to use existing amount - lf = 3 # Change the left front tire, optionally specifying the pressure in KPa or pass '0' to use existing pressure - rf = 4 # right front - lr = 5 # left rear - rr = 6 # right rear - -class TelemCommandMode: # You can call this any time, but telemtry only records when driver is in there car - stop = 0 # Turn telemetry recording off - start = 1 # Turn telemetry recording on - restart = 2 # Write current file to disk and start a new one - -class RpyStateMode: - erase_tape = 0 # clear any data in the replay tape - -class ReloadTexturesMode: - all = 0 # reload all textuers - car_idx = 1 # reload only textures for the specific carIdx - -class RpySrchMode: - to_start = 0 - to_end = 1 - prev_session = 2 - next_session = 3 - prev_lap = 4 - next_lap = 5 - prev_frame = 6 - next_frame = 7 - prev_incident = 8 - next_incident = 9 - -class RpyPosMode: - begin = 0 - current = 1 - end = 2 - -class csMode: - at_incident = -3 - at_leader = -2 - at_exciting = -1 - - - -class IRSDKStruct: - @classmethod - def property_value(cls, offset, var_type): - struct_type = struct.Struct(var_type) - return property(lambda self: self.get(offset, struct_type)) - - @classmethod - def property_value_str(cls, offset, var_type): - struct_type = struct.Struct(var_type) - return property(lambda self: self.get(offset, struct_type).strip(b'\x00').decode('latin-1')) - - def __init__(self, shared_mem, offset=0): - self._shared_mem = shared_mem - self._offset = offset - - def get(self, offset, struct_type): - return struct_type.unpack_from(self._shared_mem, self._offset + offset)[0] - -class Header(IRSDKStruct): - version = IRSDKStruct.property_value(0, 'i') - status = IRSDKStruct.property_value(4, 'i') - tick_rate = IRSDKStruct.property_value(8, 'i') - - session_info_update = IRSDKStruct.property_value(12, 'i') - session_info_len = IRSDKStruct.property_value(16, 'i') - session_info_offset = IRSDKStruct.property_value(20, 'i') - - num_vars = IRSDKStruct.property_value(24, 'i') - var_header_offset = IRSDKStruct.property_value(28, 'i') - - num_buf = IRSDKStruct.property_value(32, 'i') - buf_len = IRSDKStruct.property_value(36, 'i') - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.var_buf = [ - VarBuffer(self._shared_mem, 48 + i * 16) - for i in range(self.num_buf) - ] - -class VarBuffer(IRSDKStruct): - tick_count = IRSDKStruct.property_value(0, 'i') - buf_offset = IRSDKStruct.property_value(4, 'i') - -class VarHeader(IRSDKStruct): - type = IRSDKStruct.property_value(0, 'i') - offset = IRSDKStruct.property_value(4, 'i') - count = IRSDKStruct.property_value(8, 'i') - - name = IRSDKStruct.property_value_str(16, '32s') - desc = IRSDKStruct.property_value_str(48, '64s') - unit = IRSDKStruct.property_value_str(112, '32s') - -class IRSDK: - def __init__(self): - self.is_initialized = False - self.last_tick_count = 0 - self.last_session_info_update = 0 - - self._shared_mem = None - self._header = None - - self.__var_headers = None - self.__var_headers_dict = None - self.__var_headers_names = None - self.__session_info_dict = None - self.__broadcast_msg_id = None - - def __getitem__(self, key): - if key in self._var_headers_dict: - var_header = self._var_headers_dict[key] - var_buf_latest = self._var_buffer_latest - res = struct.unpack_from( - VAR_TYPE_MAP[var_header.type] * var_header.count, - self._shared_mem, - var_buf_latest.buf_offset + var_header.offset) - return res[0] if var_header.count == 1 else list(res) - - return self._get_session_info(key) - - @property - def is_connected(self): - return self._header and self._header.status == StatusField.status_connected - - @property - def session_info_update(self): - return self._header.session_info_update - - @property - def var_headers_names(self): - if self.__var_headers_names is None: - self.__var_headers_names = [var_header.name for var_header in self._var_headers] - return self.__var_headers_names - - def startup(self, test_file=None, dump_to=None): - if test_file is None and not self._check_sim_status(): - return False - - if self._shared_mem is None: - if test_file: - f = open(test_file, 'rb') - self._shared_mem = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - else: - self._shared_mem = mmap.mmap(0, MEMMAPFILESIZE, MEMMAPFILE, access=mmap.ACCESS_READ) - - if self._shared_mem: - if dump_to: - f = open(dump_to, 'wb') - f.write(self._shared_mem) - f.close() - self._header = Header(self._shared_mem) - self.is_initialized = self._header.version == 1 and len(self._header.var_buf) > 0 - - return self.is_initialized - - def shutdown(self): - self.is_initialized = False - self.last_tick_count = 0 - self.last_session_info_update = 0 - if self._shared_mem: - self._shared_mem.close() - self._shared_mem = None - self._header = None - self.__var_headers = None - self.__var_headers_dict = None - self.__var_headers_names = None - self.__session_info_dict = None - self.__broadcast_msg_id = None - - def parse_to(self, to_file): - if not self.is_initialized: - return - f = open(to_file, 'w', encoding='utf-8') - f.write(self._shared_mem[self._header.session_info_offset:self._header.session_info_len].rstrip(b'\x00').decode('latin-1')) - f.write('\n'.join([ - '{:32}{}'.format(i, self[i]) - for i in sorted(self._var_headers_dict.keys(), key=str.lower) - ])) - f.close() - - def cam_switch_pos(self, position=0, group=1, camera=0): - return self._broadcast_msg(BroadcastMsg.cam_switch_pos, position, group, camera) - - def cam_switch_num(self, car_number=0, group=1, camera=0): - return self._broadcast_msg(BroadcastMsg.cam_switch_num, self._pad_car_num(car_number), group, camera) - - def cam_set_state(self, camera_state=CameraState.cam_tool_active): - return self._broadcast_msg(BroadcastMsg.cam_set_state, camera_state) - - def replay_set_play_speed(self, speed=0, slow_motion=False): - return self._broadcast_msg(BroadcastMsg.replay_set_play_speed, speed, 1 if slow_motion else 0) - - def replay_set_play_position(self, pos_mode=RpyPosMode.begin, frame_num=0): - return self._broadcast_msg(BroadcastMsg.replay_set_play_position, pos_mode, frame_num) - - def replay_search(self, search_mode=RpySrchMode.to_start): - return self._broadcast_msg(BroadcastMsg.replay_search, search_mode) - - def replay_set_state(self, state_mode=RpyStateMode.erase_tape): - return self._broadcast_msg(BroadcastMsg.replay_set_state, state_mode) - - def reload_all_textures(self): - return self._broadcast_msg(BroadcastMsg.reload_textures, ReloadTexturesMode.all) - - def reload_texture(self, car_idx=0): - return self._broadcast_msg(BroadcastMsg.reload_textures, ReloadTexturesMode.car_idx, car_idx) - - def chat_command(self, chat_command_mode=ChatCommandMode.begin_chat): - return self._broadcast_msg(BroadcastMsg.chat_command, chat_command_mode) - - def chat_command_macro(self, macro_num=0): - return self._broadcast_msg(BroadcastMsg.chat_command, ChatCommandMode.macro, macro_num) - - def pit_command(self, pit_command_mode=PitCommandMode.clear, var=0): - return self._broadcast_msg(BroadcastMsg.pit_command, pit_command_mode, var) - - def telem_command(self, telem_command_mode=TelemCommandMode.stop): - return self._broadcast_msg(BroadcastMsg.telem_command, telem_command_mode) - - def _check_sim_status(self): - return 'running:1' in request.urlopen(SIM_STATUS_URL).read().decode('utf-8') - - @property - def _var_buffer_latest(self): - if not self.is_initialized and not self.startup(): - self.last_tick_count = 0 - return None - - var_buf = self._header.var_buf - var_buf_latest = var_buf[0] - for i in range(1, self._header.num_buf): - if var_buf_latest.tick_count < var_buf[i].tick_count: - var_buf_latest = var_buf[i] - - return var_buf_latest - - @property - def _var_headers(self): - if self.__var_headers is None: - self.__var_headers = [] - for i in range(self._header.num_vars): - var_header = VarHeader(self._shared_mem, self._header.var_header_offset + i * 144) - self._var_headers.append(var_header) - return self.__var_headers - - @property - def _var_headers_dict(self): - if self.__var_headers_dict is None: - self.__var_headers_dict = {} - for var_header in self._var_headers: - self.__var_headers_dict[var_header.name] = var_header - return self.__var_headers_dict - - def _get_session_info(self, key=None): - if self.last_session_info_update < self._header.session_info_update: - self.last_session_info_update = self._header.session_info_update - self.__session_info_dict = {} - - if key is None: - self.__session_info_dict = {} - - if key not in self.__session_info_dict: - start = self._header.session_info_offset - end = self._header.session_info_len - - if key is not None: - self._shared_mem.seek(0) - start = self._shared_mem.find(('\n%s:\n' % key).encode('latin-1'), start, end) - end = self._shared_mem.find(b'\n\n', start, end) - - if start != -1 and end != -1: - yaml_src = re.sub(YamlReader.NON_PRINTABLE, '', self._shared_mem[start:end].rstrip(b'\x00').decode('latin-1')) - result = yaml.load(yaml_src, Loader=YamlLoader) - if result: - self.__session_info_dict.update(result) - elif key is not None: - self.__session_info_dict[key] = None - - if key is None: - return self.__session_info_dict - return self.__session_info_dict.get(key) - - @property - def _broadcast_msg_id(self): - if self.__broadcast_msg_id is None: - self.__broadcast_msg_id = ctypes.windll.user32.RegisterWindowMessageW(BROADCASTMSGNAME) - return self.__broadcast_msg_id - - def _broadcast_msg(self, broadcast_type=0, var1=0, var2=0, var3=0): - return ctypes.windll.user32.SendNotifyMessageW(0xFFFF, self._broadcast_msg_id, - broadcast_type | var1 << 16, var2 | var3 << 16) - - def _pad_car_num(self, num): - num = str(num) - num_len = len(num) - zero = num_len - len(num.lstrip("0")) - if zero > 0 and num_len == zero: - zero -= 1 - num = int(num) - if zero: - num_place = 3 if num > 99 else 2 if num > 9 else 1 - return num + 1000 * (num_place + zero) - -class IBT: - def __init__(self): - self.buffers_length = 0 - - self._ibt_file = None - self._shared_mem = None - self._header = None - - self.__var_headers = None - self.__var_headers_dict = None - self.__var_headers_names = None - self.__session_info_dict = None - - def __getitem__(self, key): - return self.get(self.buffers_length - 1, key) - - @property - def file_name(self): - return self._ibt_file and self._ibt_file.name - - @property - def var_headers_names(self): - if not self._header: - return None - if self.__var_headers_names is None: - self.__var_headers_names = [var_header.name for var_header in self._var_headers] - return self.__var_headers_names - - def open(self, ibt_file): - self._ibt_file = open(ibt_file, 'rb') - self._shared_mem = mmap.mmap(self._ibt_file.fileno(), 0, access=mmap.ACCESS_READ) - self._header = Header(self._shared_mem) - self.buffers_length = int((self._shared_mem.size() - self._header.var_buf[0].buf_offset) / self._header.buf_len) - - def close(self): - if self._shared_mem: - self._shared_mem.close() - - if self._ibt_file: - self._ibt_file.close() - - self.buffers_length = 0 - - self._ibt_file = None - self._shared_mem = None - self._header = None - - self.__var_headers = None - self.__var_headers_dict = None - self.__var_headers_names = None - self.__session_info_dict = None - - def get(self, index, key): - if not self._header: - return None - if 0 > index >= self.buffers_length: - return None - if key in self._var_headers_dict: - var_header = self._var_headers_dict[key] - fmt = VAR_TYPE_MAP[var_header.type] * var_header.count - var_offset = var_header.offset + self._header.var_buf[0].buf_offset + index * self._header.buf_len - res = struct.unpack_from(fmt, self._shared_mem, var_offset) - return res[0] if var_header.count == 1 else list(res) - return None - - def get_all(self, key): - if not self._header: - return None - if key in self._var_headers_dict: - var_header = self._var_headers_dict[key] - fmt = VAR_TYPE_MAP[var_header.type] * var_header.count - var_offset = var_header.offset + self._header.var_buf[0].buf_offset - buf_len = self._header.buf_len - sigle_or_array = var_header.count == 1 - results = [] - for i in range(self.buffers_length): - res = struct.unpack_from(fmt, self._shared_mem, var_offset + i * buf_len) - results.append(res[0] if sigle_or_array else list(res)) - return results - return None - - @property - def _var_headers(self): - if not self._header: - return None - if self.__var_headers is None: - self.__var_headers = [] - for i in range(self._header.num_vars): - var_header = VarHeader(self._shared_mem, self._header.var_header_offset + i * 144) - self._var_headers.append(var_header) - return self.__var_headers - - @property - def _var_headers_dict(self): - if not self._header: - return None - if self.__var_headers_dict is None: - self.__var_headers_dict = {} - for var_header in self._var_headers: - self.__var_headers_dict[var_header.name] = var_header - return self.__var_headers_dict - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('-v', '--version', action='version', version='Python iRacing SDK %s' % VERSION, help='show version and exit') - parser.add_argument('--test', help='use test file as irsdk mmap') - parser.add_argument('--dump', help='dump irsdk mmap to file') - parser.add_argument('--parse', help='parse current irsdk mmap to file') - args = parser.parse_args() - - ir = IRSDK() - ir.startup(test_file=args.test, dump_to=args.dump) - - if args.parse: - ir.parse_to(args.parse) - -if __name__ == '__main__': - main() - diff --git a/main.py b/main.py index 1acdf5f..9039816 100644 --- a/main.py +++ b/main.py @@ -20,11 +20,8 @@ denom = 1600/log(2) -myPos = int() -myiRold = int() -actualiRDelta = int() -placements = set() -nPlacements = 10e3 +scenarios = set() +nScenarios = 10e3 # set up our command line option for debugging parser = argparse.ArgumentParser() @@ -51,22 +48,22 @@ def median(mylist): return sorts[length // 2] -def score(pos, compet, elos): - posiR= [element['elo'] for element in elos if element['place'] == pos][0] - competiR = [element['elo'] for element in elos if element['place'] == compet][0] +def score(pos, compet, scenario): + posiR= [placement['iR'] for placement in scenario if placement['place'] == pos][0] + competiR = [placement['iR'] for placement in scenario if placement['place'] == compet][0] return ((1-exp(-posiR/denom))*(exp(-competiR/denom)))/((1-exp(-competiR/denom))*(exp(-posiR/denom))+(1-exp(-posiR/denom))*(exp(-competiR/denom))) -def fudge(place, elos): - return ((len(elos))/2-place)/100 +def fudge(place, scenario): + return ((len(scenario))/2-place)/100 -def irDelta(place, elos): - return (len(elos)-place-sum(map(lambda c: score(place, c, elos), range(1, len(elos)+1)))-0.5-fudge(place, elos))*200/len(elos) +def ir_Delta(place, scenario): + return (len(scenario)-place-sum(map(lambda c: score(place, c, scenario), range(1, len(scenario)+1)))-0.5-fudge(place, scenario))*200/len(scenario) -def calc_deltas(elos): - return [irDelta(elo['place'], elos) for elo in elos] +def calc_deltas(scenario): + return [ir_Delta(placement['place'], scenario) for placement in scenario] def main(): @@ -297,12 +294,7 @@ def main(): if not mc: display.pop(1) - # Below we're using past event results to determine value of K to use later - global myPos - global myiRold - global actualiRDelta - global placements - + # Show the expected change in iRating for finishing in each position custIds = [drv['UserID'] for drv in ir['DriverInfo']['Drivers'] if drv['UserName'] != 'Pace Car'] iRatings = [drv['IRating'] @@ -310,24 +302,17 @@ def main(): iRmap = dict(zip(custIds, iRatings)) iRDelta = {} - with ProgressBar(max_value=nPlacements, prefix="Building scenarios:") as bar: - while len(placements) < nPlacements: - placements.add(tuple(choice(range(1,len(iRatings)+1), size=len(iRatings), replace=False, p=normalize(sorted(iRatings, reverse=True))))) - bar.update(len(placements)) + with ProgressBar(max_value=nScenarios, prefix="Building scenarios:") as bar: + while len(scenarios) < nScenarios: + scenario = list(choice(range(1, len(iRatings) + 1), size=len(iRatings), replace=False, p=normalize(sorted(iRatings, reverse=True)))) + scenario = [{'place': idx, 'iR': iRatings[ii]} + for (idx, ii) in zip(scenario, range(0, count))] + scenario = sorted(scenario, key=itemgetter('place')) + scenarios.add(tuple(frozenset(placement.items()) for placement in scenario)) # can't just add placement directly + bar.update(len(scenarios)) for finPos in progressbar(range(1, count+1), prefix="Calculate irDelta:"): - myEstdeltas = [] - for placement in placements: - elos = [{'place': idx, 'elo': iRatings[ii]} - for (idx, ii) in zip(placement, range(0, count))] - elos = sorted(elos, key=itemgetter('place')) - elos_ratings = [elo['elo'] for elo in elos] - myEstEloIdx = elos_ratings.index(iRmap[int(irw.custid)]) - elos_places = [elo['place'] for elo in elos] - if(finPos == elos_places[myEstEloIdx]): - estDeltas = calc_deltas(elos) - myEstdeltas.append(estDeltas[myEstEloIdx]) - + myEstdeltas = [calc_deltas([dict(placement) for placement in scenario])[finPos-1] for scenario in filter(lambda sc: dict(sc[finPos-1])['iR'] == iRmap[int(irw.custid)], scenarios)] iRDelta[finPos] = int(sum(myEstdeltas)/len(myEstdeltas)) if len(myEstdeltas) else float('nan') tab.add_column("iRDelta", list(iRDelta.values())) diff --git a/prettytable.py b/prettytable.py deleted file mode 100644 index 8abb952..0000000 --- a/prettytable.py +++ /dev/null @@ -1,1475 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c) 2009-2013, Luke Maurits -# All rights reserved. -# With contributions from: -# * Chris Clark -# * Klein Stephane -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The name of the author may not be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -__version__ = "0.7.2" - -import copy -import csv -import random -import re -import sys -import textwrap -import itertools -import unicodedata - -py3k = sys.version_info[0] >= 3 -if py3k: - unicode = str - basestring = str - itermap = map - iterzip = zip - uni_chr = chr - from html.parser import HTMLParser -else: - itermap = itertools.imap - iterzip = itertools.izip - uni_chr = unichr - from HTMLParser import HTMLParser - -if py3k and sys.version_info[1] >= 2: - from html import escape -else: - from cgi import escape - -# hrule styles -FRAME = 0 -ALL = 1 -NONE = 2 -HEADER = 3 - -# Table styles -DEFAULT = 10 -MSWORD_FRIENDLY = 11 -PLAIN_COLUMNS = 12 -RANDOM = 20 - -_re = re.compile("\033\[[0-9;]*m") - -def _get_size(text): - lines = text.split("\n") - height = len(lines) - width = max([_str_block_width(line) for line in lines]) - return (width, height) - -class PrettyTable(object): - - def __init__(self, field_names=None, **kwargs): - - """Return a new PrettyTable instance - - Arguments: - - encoding - Unicode encoding scheme used to decode any encoded input - field_names - list or tuple of field names - fields - list or tuple of field names to include in displays - start - index of first data row to include in output - end - index of last data row to include in output PLUS ONE (list slice style) - header - print a header showing field names (True or False) - header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None) - border - print a border around the table (True or False) - hrules - controls printing of horizontal rules after rows. Allowed values: FRAME, HEADER, ALL, NONE - vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE - int_format - controls formatting of integer data - float_format - controls formatting of floating point data - padding_width - number of spaces on either side of column data (only used if left and right paddings are None) - left_padding_width - number of spaces on left hand side of column data - right_padding_width - number of spaces on right hand side of column data - vertical_char - single character string used to draw vertical lines - horizontal_char - single character string used to draw horizontal lines - junction_char - single character string used to draw line junctions - sortby - name of field to sort rows by - sort_key - sorting key function, applied to data points before sorting - valign - default valign for each row (None, "t", "m" or "b") - reversesort - True or False to sort in descending or ascending order""" - - self.encoding = kwargs.get("encoding", "UTF-8") - - # Data - self._field_names = [] - self._align = {} - self._valign = {} - self._max_width = {} - self._rows = [] - if field_names: - self.field_names = field_names - else: - self._widths = [] - - # Options - self._options = "start end fields header border sortby reversesort sort_key attributes format hrules vrules".split() - self._options.extend("int_format float_format padding_width left_padding_width right_padding_width".split()) - self._options.extend("vertical_char horizontal_char junction_char header_style valign xhtml print_empty".split()) - for option in self._options: - if option in kwargs: - self._validate_option(option, kwargs[option]) - else: - kwargs[option] = None - - self._start = kwargs["start"] or 0 - self._end = kwargs["end"] or None - self._fields = kwargs["fields"] or None - - if kwargs["header"] in (True, False): - self._header = kwargs["header"] - else: - self._header = True - self._header_style = kwargs["header_style"] or None - if kwargs["border"] in (True, False): - self._border = kwargs["border"] - else: - self._border = True - self._hrules = kwargs["hrules"] or FRAME - self._vrules = kwargs["vrules"] or ALL - - self._sortby = kwargs["sortby"] or None - if kwargs["reversesort"] in (True, False): - self._reversesort = kwargs["reversesort"] - else: - self._reversesort = False - self._sort_key = kwargs["sort_key"] or (lambda x: x) - - self._int_format = kwargs["int_format"] or {} - self._float_format = kwargs["float_format"] or {} - self._padding_width = kwargs["padding_width"] or 1 - self._left_padding_width = kwargs["left_padding_width"] or None - self._right_padding_width = kwargs["right_padding_width"] or None - - self._vertical_char = kwargs["vertical_char"] or self._unicode("|") - self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-") - self._junction_char = kwargs["junction_char"] or self._unicode("+") - - if kwargs["print_empty"] in (True, False): - self._print_empty = kwargs["print_empty"] - else: - self._print_empty = True - self._format = kwargs["format"] or False - self._xhtml = kwargs["xhtml"] or False - self._attributes = kwargs["attributes"] or {} - - def _unicode(self, value): - if not isinstance(value, basestring): - value = str(value) - if not isinstance(value, unicode): - value = unicode(value, self.encoding, "strict") - return value - - def _justify(self, text, width, align): - excess = width - _str_block_width(text) - if align == "l": - return text + excess * " " - elif align == "r": - return excess * " " + text - else: - if excess % 2: - # Uneven padding - # Put more space on right if text is of odd length... - if _str_block_width(text) % 2: - return (excess//2)*" " + text + (excess//2 + 1)*" " - # and more space on left if text is of even length - else: - return (excess//2 + 1)*" " + text + (excess//2)*" " - # Why distribute extra space this way? To match the behaviour of - # the inbuilt str.center() method. - else: - # Equal padding on either side - return (excess//2)*" " + text + (excess//2)*" " - - def __getattr__(self, name): - - if name == "rowcount": - return len(self._rows) - elif name == "colcount": - if self._field_names: - return len(self._field_names) - elif self._rows: - return len(self._rows[0]) - else: - return 0 - else: - raise AttributeError(name) - - def __getitem__(self, index): - - new = PrettyTable() - new.field_names = self.field_names - for attr in self._options: - setattr(new, "_"+attr, getattr(self, "_"+attr)) - setattr(new, "_align", getattr(self, "_align")) - if isinstance(index, slice): - for row in self._rows[index]: - new.add_row(row) - elif isinstance(index, int): - new.add_row(self._rows[index]) - else: - raise Exception("Index %s is invalid, must be an integer or slice" % str(index)) - return new - - if py3k: - def __str__(self): - return self.__unicode__() - else: - def __str__(self): - return self.__unicode__().encode(self.encoding) - - def __unicode__(self): - return self.get_string() - - ############################## - # ATTRIBUTE VALIDATORS # - ############################## - - # The method _validate_option is all that should be used elsewhere in the code base to validate options. - # It will call the appropriate validation method for that option. The individual validation methods should - # never need to be called directly (although nothing bad will happen if they *are*). - # Validation happens in TWO places. - # Firstly, in the property setters defined in the ATTRIBUTE MANAGMENT section. - # Secondly, in the _get_options method, where keyword arguments are mixed with persistent settings - - def _validate_option(self, option, val): - if option in ("field_names"): - self._validate_field_names(val) - elif option in ("start", "end", "max_width", "padding_width", "left_padding_width", "right_padding_width", "format"): - self._validate_nonnegative_int(option, val) - elif option in ("sortby"): - self._validate_field_name(option, val) - elif option in ("sort_key"): - self._validate_function(option, val) - elif option in ("hrules"): - self._validate_hrules(option, val) - elif option in ("vrules"): - self._validate_vrules(option, val) - elif option in ("fields"): - self._validate_all_field_names(option, val) - elif option in ("header", "border", "reversesort", "xhtml", "print_empty"): - self._validate_true_or_false(option, val) - elif option in ("header_style"): - self._validate_header_style(val) - elif option in ("int_format"): - self._validate_int_format(option, val) - elif option in ("float_format"): - self._validate_float_format(option, val) - elif option in ("vertical_char", "horizontal_char", "junction_char"): - self._validate_single_char(option, val) - elif option in ("attributes"): - self._validate_attributes(option, val) - else: - raise Exception("Unrecognised option: %s!" % option) - - def _validate_field_names(self, val): - # Check for appropriate length - if self._field_names: - try: - assert len(val) == len(self._field_names) - except AssertionError: - raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._field_names))) - if self._rows: - try: - assert len(val) == len(self._rows[0]) - except AssertionError: - raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._rows[0]))) - # Check for uniqueness - try: - assert len(val) == len(set(val)) - except AssertionError: - raise Exception("Field names must be unique!") - - def _validate_header_style(self, val): - try: - assert val in ("cap", "title", "upper", "lower", None) - except AssertionError: - raise Exception("Invalid header style, use cap, title, upper, lower or None!") - - def _validate_align(self, val): - try: - assert val in ["l","c","r"] - except AssertionError: - raise Exception("Alignment %s is invalid, use l, c or r!" % val) - - def _validate_valign(self, val): - try: - assert val in ["t","m","b",None] - except AssertionError: - raise Exception("Alignment %s is invalid, use t, m, b or None!" % val) - - def _validate_nonnegative_int(self, name, val): - try: - assert int(val) >= 0 - except AssertionError: - raise Exception("Invalid value for %s: %s!" % (name, self._unicode(val))) - - def _validate_true_or_false(self, name, val): - try: - assert val in (True, False) - except AssertionError: - raise Exception("Invalid value for %s! Must be True or False." % name) - - def _validate_int_format(self, name, val): - if val == "": - return - try: - assert type(val) in (str, unicode) - assert val.isdigit() - except AssertionError: - raise Exception("Invalid value for %s! Must be an integer format string." % name) - - def _validate_float_format(self, name, val): - if val == "": - return - try: - assert type(val) in (str, unicode) - assert "." in val - bits = val.split(".") - assert len(bits) <= 2 - assert bits[0] == "" or bits[0].isdigit() - assert bits[1] == "" or bits[1].isdigit() - except AssertionError: - raise Exception("Invalid value for %s! Must be a float format string." % name) - - def _validate_function(self, name, val): - try: - assert hasattr(val, "__call__") - except AssertionError: - raise Exception("Invalid value for %s! Must be a function." % name) - - def _validate_hrules(self, name, val): - try: - assert val in (ALL, FRAME, HEADER, NONE) - except AssertionError: - raise Exception("Invalid value for %s! Must be ALL, FRAME, HEADER or NONE." % name) - - def _validate_vrules(self, name, val): - try: - assert val in (ALL, FRAME, NONE) - except AssertionError: - raise Exception("Invalid value for %s! Must be ALL, FRAME, or NONE." % name) - - def _validate_field_name(self, name, val): - try: - assert (val in self._field_names) or (val is None) - except AssertionError: - raise Exception("Invalid field name: %s!" % val) - - def _validate_all_field_names(self, name, val): - try: - for x in val: - self._validate_field_name(name, x) - except AssertionError: - raise Exception("fields must be a sequence of field names!") - - def _validate_single_char(self, name, val): - try: - assert _str_block_width(val) == 1 - except AssertionError: - raise Exception("Invalid value for %s! Must be a string of length 1." % name) - - def _validate_attributes(self, name, val): - try: - assert isinstance(val, dict) - except AssertionError: - raise Exception("attributes must be a dictionary of name/value pairs!") - - ############################## - # ATTRIBUTE MANAGEMENT # - ############################## - - def _get_field_names(self): - return self._field_names - """The names of the fields - - Arguments: - - fields - list or tuple of field names""" - def _set_field_names(self, val): - val = [self._unicode(x) for x in val] - self._validate_option("field_names", val) - if self._field_names: - old_names = self._field_names[:] - self._field_names = val - if self._align and old_names: - for old_name, new_name in zip(old_names, val): - self._align[new_name] = self._align[old_name] - for old_name in old_names: - if old_name not in self._align: - self._align.pop(old_name) - else: - for field in self._field_names: - self._align[field] = "c" - if self._valign and old_names: - for old_name, new_name in zip(old_names, val): - self._valign[new_name] = self._valign[old_name] - for old_name in old_names: - if old_name not in self._valign: - self._valign.pop(old_name) - else: - for field in self._field_names: - self._valign[field] = "t" - field_names = property(_get_field_names, _set_field_names) - - def _get_align(self): - return self._align - def _set_align(self, val): - self._validate_align(val) - for field in self._field_names: - self._align[field] = val - align = property(_get_align, _set_align) - - def _get_valign(self): - return self._valign - def _set_valign(self, val): - self._validate_valign(val) - for field in self._field_names: - self._valign[field] = val - valign = property(_get_valign, _set_valign) - - def _get_max_width(self): - return self._max_width - def _set_max_width(self, val): - self._validate_option("max_width", val) - for field in self._field_names: - self._max_width[field] = val - max_width = property(_get_max_width, _set_max_width) - - def _get_fields(self): - """List or tuple of field names to include in displays - - Arguments: - - fields - list or tuple of field names to include in displays""" - return self._fields - def _set_fields(self, val): - self._validate_option("fields", val) - self._fields = val - fields = property(_get_fields, _set_fields) - - def _get_start(self): - """Start index of the range of rows to print - - Arguments: - - start - index of first data row to include in output""" - return self._start - - def _set_start(self, val): - self._validate_option("start", val) - self._start = val - start = property(_get_start, _set_start) - - def _get_end(self): - """End index of the range of rows to print - - Arguments: - - end - index of last data row to include in output PLUS ONE (list slice style)""" - return self._end - def _set_end(self, val): - self._validate_option("end", val) - self._end = val - end = property(_get_end, _set_end) - - def _get_sortby(self): - """Name of field by which to sort rows - - Arguments: - - sortby - field name to sort by""" - return self._sortby - def _set_sortby(self, val): - self._validate_option("sortby", val) - self._sortby = val - sortby = property(_get_sortby, _set_sortby) - - def _get_reversesort(self): - """Controls direction of sorting (ascending vs descending) - - Arguments: - - reveresort - set to True to sort by descending order, or False to sort by ascending order""" - return self._reversesort - def _set_reversesort(self, val): - self._validate_option("reversesort", val) - self._reversesort = val - reversesort = property(_get_reversesort, _set_reversesort) - - def _get_sort_key(self): - """Sorting key function, applied to data points before sorting - - Arguments: - - sort_key - a function which takes one argument and returns something to be sorted""" - return self._sort_key - def _set_sort_key(self, val): - self._validate_option("sort_key", val) - self._sort_key = val - sort_key = property(_get_sort_key, _set_sort_key) - - def _get_header(self): - """Controls printing of table header with field names - - Arguments: - - header - print a header showing field names (True or False)""" - return self._header - def _set_header(self, val): - self._validate_option("header", val) - self._header = val - header = property(_get_header, _set_header) - - def _get_header_style(self): - """Controls stylisation applied to field names in header - - Arguments: - - header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None)""" - return self._header_style - def _set_header_style(self, val): - self._validate_header_style(val) - self._header_style = val - header_style = property(_get_header_style, _set_header_style) - - def _get_border(self): - """Controls printing of border around table - - Arguments: - - border - print a border around the table (True or False)""" - return self._border - def _set_border(self, val): - self._validate_option("border", val) - self._border = val - border = property(_get_border, _set_border) - - def _get_hrules(self): - """Controls printing of horizontal rules after rows - - Arguments: - - hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE""" - return self._hrules - def _set_hrules(self, val): - self._validate_option("hrules", val) - self._hrules = val - hrules = property(_get_hrules, _set_hrules) - - def _get_vrules(self): - """Controls printing of vertical rules between columns - - Arguments: - - vrules - vertical rules style. Allowed values: FRAME, ALL, NONE""" - return self._vrules - def _set_vrules(self, val): - self._validate_option("vrules", val) - self._vrules = val - vrules = property(_get_vrules, _set_vrules) - - def _get_int_format(self): - """Controls formatting of integer data - Arguments: - - int_format - integer format string""" - return self._int_format - def _set_int_format(self, val): -# self._validate_option("int_format", val) - for field in self._field_names: - self._int_format[field] = val - int_format = property(_get_int_format, _set_int_format) - - def _get_float_format(self): - """Controls formatting of floating point data - Arguments: - - float_format - floating point format string""" - return self._float_format - def _set_float_format(self, val): -# self._validate_option("float_format", val) - for field in self._field_names: - self._float_format[field] = val - float_format = property(_get_float_format, _set_float_format) - - def _get_padding_width(self): - """The number of empty spaces between a column's edge and its content - - Arguments: - - padding_width - number of spaces, must be a positive integer""" - return self._padding_width - def _set_padding_width(self, val): - self._validate_option("padding_width", val) - self._padding_width = val - padding_width = property(_get_padding_width, _set_padding_width) - - def _get_left_padding_width(self): - """The number of empty spaces between a column's left edge and its content - - Arguments: - - left_padding - number of spaces, must be a positive integer""" - return self._left_padding_width - def _set_left_padding_width(self, val): - self._validate_option("left_padding_width", val) - self._left_padding_width = val - left_padding_width = property(_get_left_padding_width, _set_left_padding_width) - - def _get_right_padding_width(self): - """The number of empty spaces between a column's right edge and its content - - Arguments: - - right_padding - number of spaces, must be a positive integer""" - return self._right_padding_width - def _set_right_padding_width(self, val): - self._validate_option("right_padding_width", val) - self._right_padding_width = val - right_padding_width = property(_get_right_padding_width, _set_right_padding_width) - - def _get_vertical_char(self): - """The charcter used when printing table borders to draw vertical lines - - Arguments: - - vertical_char - single character string used to draw vertical lines""" - return self._vertical_char - def _set_vertical_char(self, val): - val = self._unicode(val) - self._validate_option("vertical_char", val) - self._vertical_char = val - vertical_char = property(_get_vertical_char, _set_vertical_char) - - def _get_horizontal_char(self): - """The charcter used when printing table borders to draw horizontal lines - - Arguments: - - horizontal_char - single character string used to draw horizontal lines""" - return self._horizontal_char - def _set_horizontal_char(self, val): - val = self._unicode(val) - self._validate_option("horizontal_char", val) - self._horizontal_char = val - horizontal_char = property(_get_horizontal_char, _set_horizontal_char) - - def _get_junction_char(self): - """The charcter used when printing table borders to draw line junctions - - Arguments: - - junction_char - single character string used to draw line junctions""" - return self._junction_char - def _set_junction_char(self, val): - val = self._unicode(val) - self._validate_option("vertical_char", val) - self._junction_char = val - junction_char = property(_get_junction_char, _set_junction_char) - - def _get_format(self): - """Controls whether or not HTML tables are formatted to match styling options - - Arguments: - - format - True or False""" - return self._format - def _set_format(self, val): - self._validate_option("format", val) - self._format = val - format = property(_get_format, _set_format) - - def _get_print_empty(self): - """Controls whether or not empty tables produce a header and frame or just an empty string - - Arguments: - - print_empty - True or False""" - return self._print_empty - def _set_print_empty(self, val): - self._validate_option("print_empty", val) - self._print_empty = val - print_empty = property(_get_print_empty, _set_print_empty) - - def _get_attributes(self): - """A dictionary of HTML attribute name/value pairs to be included in the tag when printing HTML - - Arguments: - - attributes - dictionary of attributes""" - return self._attributes - def _set_attributes(self, val): - self._validate_option("attributes", val) - self._attributes = val - attributes = property(_get_attributes, _set_attributes) - - ############################## - # OPTION MIXER # - ############################## - - def _get_options(self, kwargs): - - options = {} - for option in self._options: - if option in kwargs: - self._validate_option(option, kwargs[option]) - options[option] = kwargs[option] - else: - options[option] = getattr(self, "_"+option) - return options - - ############################## - # PRESET STYLE LOGIC # - ############################## - - def set_style(self, style): - - if style == DEFAULT: - self._set_default_style() - elif style == MSWORD_FRIENDLY: - self._set_msword_style() - elif style == PLAIN_COLUMNS: - self._set_columns_style() - elif style == RANDOM: - self._set_random_style() - else: - raise Exception("Invalid pre-set style!") - - def _set_default_style(self): - - self.header = True - self.border = True - self._hrules = FRAME - self._vrules = ALL - self.padding_width = 1 - self.left_padding_width = 1 - self.right_padding_width = 1 - self.vertical_char = "|" - self.horizontal_char = "-" - self.junction_char = "+" - - def _set_msword_style(self): - - self.header = True - self.border = True - self._hrules = NONE - self.padding_width = 1 - self.left_padding_width = 1 - self.right_padding_width = 1 - self.vertical_char = "|" - - def _set_columns_style(self): - - self.header = True - self.border = False - self.padding_width = 1 - self.left_padding_width = 0 - self.right_padding_width = 8 - - def _set_random_style(self): - - # Just for fun! - self.header = random.choice((True, False)) - self.border = random.choice((True, False)) - self._hrules = random.choice((ALL, FRAME, HEADER, NONE)) - self._vrules = random.choice((ALL, FRAME, NONE)) - self.left_padding_width = random.randint(0,5) - self.right_padding_width = random.randint(0,5) - self.vertical_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") - self.horizontal_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") - self.junction_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") - - ############################## - # DATA INPUT METHODS # - ############################## - - def add_row(self, row): - - """Add a row to the table - - Arguments: - - row - row of data, should be a list with as many elements as the table - has fields""" - - if self._field_names and len(row) != len(self._field_names): - raise Exception("Row has incorrect number of values, (actual) %d!=%d (expected)" %(len(row),len(self._field_names))) - if not self._field_names: - self.field_names = [("Field %d" % (n+1)) for n in range(0,len(row))] - self._rows.append(list(row)) - - def del_row(self, row_index): - - """Delete a row to the table - - Arguments: - - row_index - The index of the row you want to delete. Indexing starts at 0.""" - - if row_index > len(self._rows)-1: - raise Exception("Cant delete row at index %d, table only has %d rows!" % (row_index, len(self._rows))) - del self._rows[row_index] - - def add_column(self, fieldname, column, align="c", valign="t"): - - """Add a column to the table. - - Arguments: - - fieldname - name of the field to contain the new column of data - column - column of data, should be a list with as many elements as the - table has rows - align - desired alignment for this column - "l" for left, "c" for centre and "r" for right - valign - desired vertical alignment for new columns - "t" for top, "m" for middle and "b" for bottom""" - - if len(self._rows) in (0, len(column)): - self._validate_align(align) - self._validate_valign(valign) - self._field_names.append(fieldname) - self._align[fieldname] = align - self._valign[fieldname] = valign - for i in range(0, len(column)): - if len(self._rows) < i+1: - self._rows.append([]) - self._rows[i].append(column[i]) - else: - raise Exception("Column length %d does not match number of rows %d!" % (len(column), len(self._rows))) - - def clear_rows(self): - - """Delete all rows from the table but keep the current field names""" - - self._rows = [] - - def clear(self): - - """Delete all rows and field names from the table, maintaining nothing but styling options""" - - self._rows = [] - self._field_names = [] - self._widths = [] - - ############################## - # MISC PUBLIC METHODS # - ############################## - - def copy(self): - return copy.deepcopy(self) - - ############################## - # MISC PRIVATE METHODS # - ############################## - - def _format_value(self, field, value): - if isinstance(value, int) and field in self._int_format: - value = self._unicode(("%%%sd" % self._int_format[field]) % value) - elif isinstance(value, float) and field in self._float_format: - value = self._unicode(("%%%sf" % self._float_format[field]) % value) - return self._unicode(value) - - def _compute_widths(self, rows, options): - if options["header"]: - widths = [_get_size(field)[0] for field in self._field_names] - else: - widths = len(self.field_names) * [0] - for row in rows: - for index, value in enumerate(row): - fieldname = self.field_names[index] - if fieldname in self.max_width: - widths[index] = max(widths[index], min(_get_size(value)[0], self.max_width[fieldname])) - else: - widths[index] = max(widths[index], _get_size(value)[0]) - self._widths = widths - - def _get_padding_widths(self, options): - - if options["left_padding_width"] is not None: - lpad = options["left_padding_width"] - else: - lpad = options["padding_width"] - if options["right_padding_width"] is not None: - rpad = options["right_padding_width"] - else: - rpad = options["padding_width"] - return lpad, rpad - - def _get_rows(self, options): - """Return only those data rows that should be printed, based on slicing and sorting. - - Arguments: - - options - dictionary of option settings.""" - - # Make a copy of only those rows in the slice range - rows = copy.deepcopy(self._rows[options["start"]:options["end"]]) - # Sort if necessary - if options["sortby"]: - sortindex = self._field_names.index(options["sortby"]) - # Decorate - rows = [[row[sortindex]]+row for row in rows] - # Sort - rows.sort(reverse=options["reversesort"], key=options["sort_key"]) - # Undecorate - rows = [row[1:] for row in rows] - return rows - - def _format_row(self, row, options): - return [self._format_value(field, value) for (field, value) in zip(self._field_names, row)] - - def _format_rows(self, rows, options): - return [self._format_row(row, options) for row in rows] - - ############################## - # PLAIN TEXT STRING METHODS # - ############################## - - def get_string(self, **kwargs): - - """Return string representation of table in current state. - - Arguments: - - start - index of first data row to include in output - end - index of last data row to include in output PLUS ONE (list slice style) - fields - names of fields (columns) to include - header - print a header showing field names (True or False) - border - print a border around the table (True or False) - hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE - vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE - int_format - controls formatting of integer data - float_format - controls formatting of floating point data - padding_width - number of spaces on either side of column data (only used if left and right paddings are None) - left_padding_width - number of spaces on left hand side of column data - right_padding_width - number of spaces on right hand side of column data - vertical_char - single character string used to draw vertical lines - horizontal_char - single character string used to draw horizontal lines - junction_char - single character string used to draw line junctions - sortby - name of field to sort rows by - sort_key - sorting key function, applied to data points before sorting - reversesort - True or False to sort in descending or ascending order - print empty - if True, stringify just the header for an empty table, if False return an empty string """ - - options = self._get_options(kwargs) - - lines = [] - - # Don't think too hard about an empty table - # Is this the desired behaviour? Maybe we should still print the header? - if self.rowcount == 0 and (not options["print_empty"] or not options["border"]): - return "" - - # Get the rows we need to print, taking into account slicing, sorting, etc. - rows = self._get_rows(options) - - # Turn all data in all rows into Unicode, formatted as desired - formatted_rows = self._format_rows(rows, options) - - # Compute column widths - self._compute_widths(formatted_rows, options) - - # Add header or top of border - self._hrule = self._stringify_hrule(options) - if options["header"]: - lines.append(self._stringify_header(options)) - elif options["border"] and options["hrules"] in (ALL, FRAME): - lines.append(self._hrule) - - # Add rows - for row in formatted_rows: - lines.append(self._stringify_row(row, options)) - - # Add bottom of border - if options["border"] and options["hrules"] == FRAME: - lines.append(self._hrule) - - return self._unicode("\n").join(lines) - - def _stringify_hrule(self, options): - - if not options["border"]: - return "" - lpad, rpad = self._get_padding_widths(options) - if options['vrules'] in (ALL, FRAME): - bits = [options["junction_char"]] - else: - bits = [options["horizontal_char"]] - # For tables with no data or fieldnames - if not self._field_names: - bits.append(options["junction_char"]) - return "".join(bits) - for field, width in zip(self._field_names, self._widths): - if options["fields"] and field not in options["fields"]: - continue - bits.append((width+lpad+rpad)*options["horizontal_char"]) - if options['vrules'] == ALL: - bits.append(options["junction_char"]) - else: - bits.append(options["horizontal_char"]) - if options["vrules"] == FRAME: - bits.pop() - bits.append(options["junction_char"]) - return "".join(bits) - - def _stringify_header(self, options): - - bits = [] - lpad, rpad = self._get_padding_widths(options) - if options["border"]: - if options["hrules"] in (ALL, FRAME): - bits.append(self._hrule) - bits.append("\n") - if options["vrules"] in (ALL, FRAME): - bits.append(options["vertical_char"]) - else: - bits.append(" ") - # For tables with no data or field names - if not self._field_names: - if options["vrules"] in (ALL, FRAME): - bits.append(options["vertical_char"]) - else: - bits.append(" ") - for field, width, in zip(self._field_names, self._widths): - if options["fields"] and field not in options["fields"]: - continue - if self._header_style == "cap": - fieldname = field.capitalize() - elif self._header_style == "title": - fieldname = field.title() - elif self._header_style == "upper": - fieldname = field.upper() - elif self._header_style == "lower": - fieldname = field.lower() - else: - fieldname = field - bits.append(" " * lpad + self._justify(fieldname, width, self._align[field]) + " " * rpad) - if options["border"]: - if options["vrules"] == ALL: - bits.append(options["vertical_char"]) - else: - bits.append(" ") - # If vrules is FRAME, then we just appended a space at the end - # of the last field, when we really want a vertical character - if options["border"] and options["vrules"] == FRAME: - bits.pop() - bits.append(options["vertical_char"]) - if options["border"] and options["hrules"] != NONE: - bits.append("\n") - bits.append(self._hrule) - return "".join(bits) - - def _stringify_row(self, row, options): - - for index, field, value, width, in zip(range(0,len(row)), self._field_names, row, self._widths): - # Enforce max widths - lines = value.split("\n") - new_lines = [] - for line in lines: - if _str_block_width(line) > width: - line = textwrap.fill(line, width) - new_lines.append(line) - lines = new_lines - value = "\n".join(lines) - row[index] = value - - row_height = 0 - for c in row: - h = _get_size(c)[1] - if h > row_height: - row_height = h - - bits = [] - lpad, rpad = self._get_padding_widths(options) - for y in range(0, row_height): - bits.append([]) - if options["border"]: - if options["vrules"] in (ALL, FRAME): - bits[y].append(self.vertical_char) - else: - bits[y].append(" ") - - for field, value, width, in zip(self._field_names, row, self._widths): - - valign = self._valign[field] - lines = value.split("\n") - dHeight = row_height - len(lines) - if dHeight: - if valign == "m": - lines = [""] * int(dHeight / 2) + lines + [""] * (dHeight - int(dHeight / 2)) - elif valign == "b": - lines = [""] * dHeight + lines - else: - lines = lines + [""] * dHeight - - y = 0 - for l in lines: - if options["fields"] and field not in options["fields"]: - continue - - bits[y].append(" " * lpad + self._justify(l, width, self._align[field]) + " " * rpad) - if options["border"]: - if options["vrules"] == ALL: - bits[y].append(self.vertical_char) - else: - bits[y].append(" ") - y += 1 - - # If vrules is FRAME, then we just appended a space at the end - # of the last field, when we really want a vertical character - for y in range(0, row_height): - if options["border"] and options["vrules"] == FRAME: - bits[y].pop() - bits[y].append(options["vertical_char"]) - - if options["border"] and options["hrules"]== ALL: - bits[row_height-1].append("\n") - bits[row_height-1].append(self._hrule) - - for y in range(0, row_height): - bits[y] = "".join(bits[y]) - - return "\n".join(bits) - - ############################## - # HTML STRING METHODS # - ############################## - - def get_html_string(self, **kwargs): - - """Return string representation of HTML formatted version of table in current state. - - Arguments: - - start - index of first data row to include in output - end - index of last data row to include in output PLUS ONE (list slice style) - fields - names of fields (columns) to include - header - print a header showing field names (True or False) - border - print a border around the table (True or False) - hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE - vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE - int_format - controls formatting of integer data - float_format - controls formatting of floating point data - padding_width - number of spaces on either side of column data (only used if left and right paddings are None) - left_padding_width - number of spaces on left hand side of column data - right_padding_width - number of spaces on right hand side of column data - sortby - name of field to sort rows by - sort_key - sorting key function, applied to data points before sorting - attributes - dictionary of name/value pairs to include as HTML attributes in the
tag - xhtml - print
tags if True,
tags if false""" - - options = self._get_options(kwargs) - - if options["format"]: - string = self._get_formatted_html_string(options) - else: - string = self._get_simple_html_string(options) - - return string - - def _get_simple_html_string(self, options): - - lines = [] - if options["xhtml"]: - linebreak = "
" - else: - linebreak = "
" - - open_tag = [] - open_tag.append("") - lines.append("".join(open_tag)) - - # Headers - if options["header"]: - lines.append(" ") - for field in self._field_names: - if options["fields"] and field not in options["fields"]: - continue - lines.append(" " % escape(field).replace("\n", linebreak)) - lines.append(" ") - - # Data - rows = self._get_rows(options) - formatted_rows = self._format_rows(rows, options) - for row in formatted_rows: - lines.append(" ") - for field, datum in zip(self._field_names, row): - if options["fields"] and field not in options["fields"]: - continue - lines.append(" " % escape(datum).replace("\n", linebreak)) - lines.append(" ") - - lines.append("
%s
%s
") - - return self._unicode("\n").join(lines) - - def _get_formatted_html_string(self, options): - - lines = [] - lpad, rpad = self._get_padding_widths(options) - if options["xhtml"]: - linebreak = "
" - else: - linebreak = "
" - - open_tag = [] - open_tag.append("") - lines.append("".join(open_tag)) - - # Headers - if options["header"]: - lines.append(" ") - for field in self._field_names: - if options["fields"] and field not in options["fields"]: - continue - lines.append(" %s" % (lpad, rpad, escape(field).replace("\n", linebreak))) - lines.append(" ") - - # Data - rows = self._get_rows(options) - formatted_rows = self._format_rows(rows, options) - aligns = [] - valigns = [] - for field in self._field_names: - aligns.append({ "l" : "left", "r" : "right", "c" : "center" }[self._align[field]]) - valigns.append({"t" : "top", "m" : "middle", "b" : "bottom"}[self._valign[field]]) - for row in formatted_rows: - lines.append(" ") - for field, datum, align, valign in zip(self._field_names, row, aligns, valigns): - if options["fields"] and field not in options["fields"]: - continue - lines.append(" %s" % (lpad, rpad, align, valign, escape(datum).replace("\n", linebreak))) - lines.append(" ") - lines.append("") - - return self._unicode("\n").join(lines) - -############################## -# UNICODE WIDTH FUNCTIONS # -############################## - -def _char_block_width(char): - # Basic Latin, which is probably the most common case - #if char in xrange(0x0021, 0x007e): - #if char >= 0x0021 and char <= 0x007e: - if 0x0021 <= char <= 0x007e: - return 1 - # Chinese, Japanese, Korean (common) - if 0x4e00 <= char <= 0x9fff: - return 2 - # Hangul - if 0xac00 <= char <= 0xd7af: - return 2 - # Combining? - if unicodedata.combining(uni_chr(char)): - return 0 - # Hiragana and Katakana - if 0x3040 <= char <= 0x309f or 0x30a0 <= char <= 0x30ff: - return 2 - # Full-width Latin characters - if 0xff01 <= char <= 0xff60: - return 2 - # CJK punctuation - if 0x3000 <= char <= 0x303e: - return 2 - # Backspace and delete - if char in (0x0008, 0x007f): - return -1 - # Other control characters - elif char in (0x0000, 0x001f): - return 0 - # Take a guess - return 1 - -def _str_block_width(val): - - return sum(itermap(_char_block_width, itermap(ord, _re.sub("", val)))) - -############################## -# TABLE FACTORIES # -############################## - -def from_csv(fp, field_names = None, **kwargs): - - dialect = csv.Sniffer().sniff(fp.read(1024)) - fp.seek(0) - reader = csv.reader(fp, dialect) - - table = PrettyTable(**kwargs) - if field_names: - table.field_names = field_names - else: - if py3k: - table.field_names = [x.strip() for x in next(reader)] - else: - table.field_names = [x.strip() for x in reader.next()] - - for row in reader: - table.add_row([x.strip() for x in row]) - - return table - -def from_db_cursor(cursor, **kwargs): - - if cursor.description: - table = PrettyTable(**kwargs) - table.field_names = [col[0] for col in cursor.description] - for row in cursor.fetchall(): - table.add_row(row) - return table - -class TableHandler(HTMLParser): - - def __init__(self, **kwargs): - HTMLParser.__init__(self) - self.kwargs = kwargs - self.tables = [] - self.last_row = [] - self.rows = [] - self.max_row_width = 0 - self.active = None - self.last_content = "" - self.is_last_row_header = False - - def handle_starttag(self,tag, attrs): - self.active = tag - if tag == "th": - self.is_last_row_header = True - - def handle_endtag(self,tag): - if tag in ["th", "td"]: - stripped_content = self.last_content.strip() - self.last_row.append(stripped_content) - if tag == "tr": - self.rows.append( - (self.last_row, self.is_last_row_header)) - self.max_row_width = max(self.max_row_width, len(self.last_row)) - self.last_row = [] - self.is_last_row_header = False - if tag == "table": - table = self.generate_table(self.rows) - self.tables.append(table) - self.rows = [] - self.last_content = " " - self.active = None - - - def handle_data(self, data): - self.last_content += data - - def generate_table(self, rows): - """ - Generates from a list of rows a PrettyTable object. - """ - table = PrettyTable(**self.kwargs) - for row in self.rows: - if len(row[0]) < self.max_row_width: - appends = self.max_row_width - len(row[0]) - for i in range(1,appends): - row[0].append("-") - - if row[1] == True: - self.make_fields_unique(row[0]) - table.field_names = row[0] - else: - table.add_row(row[0]) - return table - - def make_fields_unique(self, fields): - """ - iterates over the row and make each field unique - """ - for i in range(0, len(fields)): - for j in range(i+1, len(fields)): - if fields[i] == fields[j]: - fields[j] += "'" - -def from_html(html_code, **kwargs): - """ - Generates a list of PrettyTables from a string of HTML code. Each in - the HTML becomes one PrettyTable object. - """ - - parser = TableHandler(**kwargs) - parser.feed(html_code) - return parser.tables - -def from_html_one(html_code, **kwargs): - """ - Generates a PrettyTables from a string of HTML code which contains only a - single
- """ - - tables = from_html(html_code, **kwargs) - try: - assert len(tables) == 1 - except AssertionError: - raise Exception("More than one
in provided HTML code! Use from_html instead.") - return tables[0] - -############################## -# MAIN (TEST FUNCTION) # -############################## - -def main(): - - x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"]) - x.sortby = "Population" - x.reversesort = True - x.int_format["Area"] = "04d" - x.float_format = "6.1f" - x.align["City name"] = "l" # Left align city names - x.add_row(["Adelaide", 1295, 1158259, 600.5]) - x.add_row(["Brisbane", 5905, 1857594, 1146.4]) - x.add_row(["Darwin", 112, 120900, 1714.7]) - x.add_row(["Hobart", 1357, 205556, 619.5]) - x.add_row(["Sydney", 2058, 4336374, 1214.8]) - x.add_row(["Melbourne", 1566, 3806092, 646.9]) - x.add_row(["Perth", 5386, 1554769, 869.4]) - print(x) - -if __name__ == "__main__": - main() diff --git a/pyirsdk b/pyirsdk new file mode 160000 index 0000000..260de36 --- /dev/null +++ b/pyirsdk @@ -0,0 +1 @@ +Subproject commit 260de365cda451801b7291e5f06a6f7f790633b9 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b57b4df --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +configobj +requests +numpy +progressbar2 +prettytable diff --git a/setup.py b/setup.py index 50fc878..62962d6 100644 --- a/setup.py +++ b/setup.py @@ -156,7 +156,7 @@ def __setitem__(self, name, value): setup(name="name", # console based executables console=[main], - + includes = ['config', 'constants', 'decorator', 'iRWebStats', 'irsdk', 'prettytable', 'urllib3.request', 'util', 'simplejson'], # windows subsystem executables (no console) diff --git a/util.py b/util.py deleted file mode 100644 index 8f90980..0000000 --- a/util.py +++ /dev/null @@ -1,60 +0,0 @@ -import inspect -import decorator -import json -try: - from urllib.parse import unquote # python3 -except: - from urllib import unquote # python2 - - -def tofile(data): - a = open('output.html', 'w') - a.write(data) - a.close() - - -def format_results(results, header): - newres = [] - for row in results: - newr = {} - for k, v in row.items(): - newr[header[k]] = v - newres.append(newr) - return newres - - -def __logged_in(func, *args, **kw): - args2 = list(args) - irweb = args2[0] - if not irweb.logged: - pprint("Error, client is not logged in to iRacing Platform so\ - operation couldn't be completed.", irweb.verbose) - return None - - if 'custid' in inspect.getargspec(func).args: - args2[1] = args2[1] if args2[1] is not None else irweb.custid - - return func(*args2, **kw) - - -def logged_in(func): - return decorator.decorator(__logged_in, func) - - -def pprint(string, v=True): - if v: - print(' '.join(str(string).split())) - - -def parse(data): - res = '' - try: - res = json.loads(data) # iRacing responses are generally in JSON - except: - pass # TODO raise error? - - return res - - -def clean(string): - return unquote(string.replace('+', ' '))