loutilities 3.7.2__py3.9.egg → 3.8.1__py3.9.egg
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- EGG-INFO/PKG-INFO +1 -1
- EGG-INFO/scripts/agegrade.py +83 -78
- loutilities/__pycache__/agegrade.cpython-39.pyc +0 -0
- loutilities/__pycache__/tables.cpython-39.pyc +0 -0
- loutilities/__pycache__/version.cpython-39.pyc +0 -0
- loutilities/agegrade.py +83 -78
- loutilities/tables.py +35 -0
- loutilities/user/__init__.py +5 -5
- loutilities/user/__pycache__/__init__.cpython-39.pyc +0 -0
- loutilities/user/__pycache__/model.cpython-39.pyc +0 -0
- loutilities/version.py +1 -1
EGG-INFO/PKG-INFO
CHANGED
EGG-INFO/scripts/agegrade.py
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/python
|
|
2
|
-
###########################################################################################
|
|
3
|
-
# agegrade - calculate age grade statistics
|
|
4
|
-
#
|
|
5
|
-
# Date Author Reason
|
|
6
|
-
# ---- ------ ------
|
|
7
|
-
# 02/17/13 Lou King Create
|
|
8
|
-
# 03/20/14 Lou King Moved from runningclub
|
|
9
|
-
#
|
|
10
|
-
# Copyright 2013,2014 Lou King
|
|
11
|
-
#
|
|
12
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
-
# you may not use this file except in compliance with the License.
|
|
14
|
-
# You may obtain a copy of the License at
|
|
15
|
-
#
|
|
16
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
-
#
|
|
18
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
-
# See the License for the specific language governing permissions and
|
|
22
|
-
# limitations under the License.
|
|
23
|
-
#
|
|
24
|
-
###########################################################################################
|
|
25
1
|
'''
|
|
26
2
|
agegrade - calculate age grade statistics
|
|
27
3
|
===================================================
|
|
@@ -43,9 +19,7 @@ import shutil
|
|
|
43
19
|
class missingConfiguration(Exception): pass
|
|
44
20
|
class parameterError(Exception): pass
|
|
45
21
|
|
|
46
|
-
#----------------------------------------------------------------------
|
|
47
22
|
def getagtable(agegradewb):
|
|
48
|
-
#----------------------------------------------------------------------
|
|
49
23
|
'''
|
|
50
24
|
in return data structure:
|
|
51
25
|
|
|
@@ -56,10 +30,25 @@ def getagtable(agegradewb):
|
|
|
56
30
|
|
|
57
31
|
:param agegradewb: excel workbook containing age grade factors (e.g., from http://www.howardgrubb.co.uk/athletics/data/wavacalc10.xls)
|
|
58
32
|
|
|
59
|
-
:rtype:
|
|
33
|
+
:rtype: dict: {
|
|
34
|
+
'road: {'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},'M':{dist:{'OC':openstd,age:factor,age:factor,...},...}},
|
|
35
|
+
'trail: {'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},'M':{dist:{'OC':openstd,age:factor,age:factor,...},...}},
|
|
36
|
+
}
|
|
37
|
+
|
|
60
38
|
'''
|
|
61
39
|
|
|
62
|
-
agegradedata = {
|
|
40
|
+
agegradedata = {
|
|
41
|
+
'road': {
|
|
42
|
+
'F': {},
|
|
43
|
+
'M': {},
|
|
44
|
+
'X': {},
|
|
45
|
+
},
|
|
46
|
+
'track': {
|
|
47
|
+
'F': {},
|
|
48
|
+
'M': {},
|
|
49
|
+
'X': {},
|
|
50
|
+
},
|
|
51
|
+
}
|
|
63
52
|
|
|
64
53
|
# convert the workbook to csv
|
|
65
54
|
from .csvwt import Xls2Csv
|
|
@@ -82,9 +71,6 @@ def getagtable(agegradewb):
|
|
|
82
71
|
except ValueError:
|
|
83
72
|
pass
|
|
84
73
|
|
|
85
|
-
# create gender
|
|
86
|
-
agegradedata[gen] = {}
|
|
87
|
-
|
|
88
74
|
# add each row to data structure, but skip non-running events
|
|
89
75
|
for r in sheet:
|
|
90
76
|
if r['dist(km)'] == '0.0': continue
|
|
@@ -92,20 +78,16 @@ def getagtable(agegradewb):
|
|
|
92
78
|
# dist is rounded to the nearest meter so it can be used as a key
|
|
93
79
|
dist = int(round(float(r['dist(km)'])*1000))
|
|
94
80
|
|
|
95
|
-
# kludge to use only road events -- affects distances 5km and beyond (issue #55)
|
|
96
|
-
# this works because Howard Grubb's spreadsheet has road first, then track for events which matter
|
|
97
|
-
# as of wavacalc10.xls, includes 5k, 6k, 4M, 8k, 5M, 10k distances
|
|
98
|
-
if dist in agegradedata[gen]: continue
|
|
99
|
-
|
|
100
81
|
# create dist
|
|
82
|
+
surface = 'road' if r['isRoad'] == 1 else 'track'
|
|
101
83
|
openstd = float(r['OC'])
|
|
102
|
-
agegradedata[gen][dist] = {'OC':openstd}
|
|
84
|
+
agegradedata[surface][gen][dist] = {'OC':openstd}
|
|
103
85
|
|
|
104
86
|
# add each age factor
|
|
105
87
|
for f in sheet.fieldnames:
|
|
106
88
|
if f in f2age:
|
|
107
89
|
age = f2age[f]
|
|
108
|
-
agegradedata[gen][dist][age] = float(r[f])
|
|
90
|
+
agegradedata[surface][gen][dist][age] = float(r[f])
|
|
109
91
|
|
|
110
92
|
|
|
111
93
|
SHEET.close()
|
|
@@ -113,31 +95,41 @@ def getagtable(agegradewb):
|
|
|
113
95
|
del c
|
|
114
96
|
return agegradedata
|
|
115
97
|
|
|
116
|
-
|
|
98
|
+
|
|
117
99
|
class AgeGrade():
|
|
118
|
-
########################################################################
|
|
119
100
|
'''
|
|
120
101
|
AgeGrade object
|
|
121
102
|
|
|
122
|
-
agegradewb is in format per http://www.howardgrubb.co.uk/athletics/
|
|
123
|
-
if agegradewb parameter is missing, previous configuration is used
|
|
124
|
-
configuration is created through command line: agegrade.py [-a agworkbook | -c agconfigfile]
|
|
103
|
+
agegradewb is in format per http://www.howardgrubb.co.uk/athletics/wmalookup15.html (deprecated)
|
|
104
|
+
if agegradewb parameter is missing, previous configuration is used (deprecated)
|
|
105
|
+
configuration is created through command line: agegrade.py [-a agworkbook | -c agconfigfile] (deprecated)
|
|
125
106
|
|
|
126
|
-
:param
|
|
127
|
-
|
|
107
|
+
:param agegradedata: data structure used by this class
|
|
108
|
+
{
|
|
109
|
+
'road: {
|
|
110
|
+
'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
111
|
+
'M':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
112
|
+
'X':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
113
|
+
},
|
|
114
|
+
'trail: {
|
|
115
|
+
'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
116
|
+
'M':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
117
|
+
'X':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
:param agegradewb: (deprecated) excel workbook containing age grade factors
|
|
121
|
+
:param DEBUG: logger function for debug output
|
|
128
122
|
'''
|
|
129
|
-
|
|
130
|
-
def __init__(self,agegradewb=None,DEBUG=None):
|
|
131
|
-
#----------------------------------------------------------------------
|
|
123
|
+
def __init__(self, agegradedata=None, agegradewb=None, DEBUG=None):
|
|
132
124
|
from .config import CONFIGDIR
|
|
133
125
|
self.DEBUG = DEBUG
|
|
134
126
|
|
|
135
|
-
#
|
|
136
|
-
if
|
|
137
|
-
self.
|
|
138
|
-
|
|
127
|
+
# use age grade data structure if specified
|
|
128
|
+
if agegradedata:
|
|
129
|
+
self.agegradedata = agegradedata
|
|
130
|
+
|
|
139
131
|
# use age grade workbook if specified
|
|
140
|
-
|
|
132
|
+
elif agegradewb:
|
|
141
133
|
self.agegradedata = getagtable(agegradewb)
|
|
142
134
|
|
|
143
135
|
# otherwise, pick up the data from the configuration
|
|
@@ -150,12 +142,11 @@ class AgeGrade():
|
|
|
150
142
|
self.agegradedata = pickle.load(C)
|
|
151
143
|
C.close()
|
|
152
144
|
|
|
153
|
-
|
|
154
|
-
def getfactorstd(self,age,gen,distmeters):
|
|
155
|
-
#----------------------------------------------------------------------
|
|
145
|
+
def getfactorstd(self, surface, age, gen, distmeters):
|
|
156
146
|
'''
|
|
157
147
|
interpolate factor and openstd based on distance for this age
|
|
158
148
|
|
|
149
|
+
:param surface: 'road' or 'track'
|
|
159
150
|
:param age: integer age. If float is supplied, integer portion is used (no interpolation of fractional age)
|
|
160
151
|
:param gen: gender - M or F
|
|
161
152
|
:param distmeters: distance (meters)
|
|
@@ -171,16 +162,16 @@ class AgeGrade():
|
|
|
171
162
|
distmeters = round(distmeters)
|
|
172
163
|
|
|
173
164
|
# find surrounding Xi points, and corresponding Fi, OCi points
|
|
174
|
-
distlist = sorted(list(self.agegradedata[gen].keys()))
|
|
165
|
+
distlist = sorted(list(self.agegradedata[surface][gen].keys()))
|
|
175
166
|
lastd = distlist[0]
|
|
176
167
|
for i in range(1,len(distlist)):
|
|
177
168
|
if distmeters <= distlist[i]:
|
|
178
169
|
x0 = lastd
|
|
179
170
|
x1 = distlist[i]
|
|
180
|
-
f0 = self.agegradedata[gen][x0][age]
|
|
181
|
-
f1 = self.agegradedata[gen][x1][age]
|
|
182
|
-
oc0 = self.agegradedata[gen][x0]['OC']
|
|
183
|
-
oc1 = self.agegradedata[gen][x1]['OC']
|
|
171
|
+
f0 = self.agegradedata[surface][gen][x0][age]
|
|
172
|
+
f1 = self.agegradedata[surface][gen][x1][age]
|
|
173
|
+
oc0 = self.agegradedata[surface][gen][x0]['OC']
|
|
174
|
+
oc1 = self.agegradedata[surface][gen][x1]['OC']
|
|
184
175
|
break
|
|
185
176
|
lastd = distlist[i]
|
|
186
177
|
|
|
@@ -190,9 +181,7 @@ class AgeGrade():
|
|
|
190
181
|
|
|
191
182
|
return factor,openstd
|
|
192
183
|
|
|
193
|
-
|
|
194
|
-
def agegrade(self,age,gen,distmiles,time):
|
|
195
|
-
#----------------------------------------------------------------------
|
|
184
|
+
def agegrade(self, age, gen, distmiles, time, surface=None, errorlogger=None):
|
|
196
185
|
'''
|
|
197
186
|
returns age grade statistics for the indicated age, gender, distance, result time
|
|
198
187
|
|
|
@@ -202,6 +191,7 @@ class AgeGrade():
|
|
|
202
191
|
:param gen: gender - M, F, X
|
|
203
192
|
:param distmiles: distance (miles)
|
|
204
193
|
:param time: time for distance (seconds)
|
|
194
|
+
:param surface: (optional) 'road' or 'track', default 'road'
|
|
205
195
|
|
|
206
196
|
:rtype: (age performance percentage, age graded result, age grade factor) - percentage is between 0 and 100, result is in seconds
|
|
207
197
|
'''
|
|
@@ -223,23 +213,42 @@ class AgeGrade():
|
|
|
223
213
|
else:
|
|
224
214
|
distmeters = distmiles*mpermile
|
|
225
215
|
|
|
216
|
+
# surface might require some adjustment
|
|
217
|
+
## if surface not provided, assume road if we have road factors for this distance, else assume track
|
|
218
|
+
## this maintains backwards compatibility, or for when caller has no access to what surface a race was run on
|
|
219
|
+
initialsurface = surface
|
|
220
|
+
if not surface:
|
|
221
|
+
minroad = min(list(self.agegradedata['road'][gen].keys()))
|
|
222
|
+
# need to round distmeters here because dist keys are rounded integers
|
|
223
|
+
if int(round(distmeters)) >= minroad:
|
|
224
|
+
surface = 'road'
|
|
225
|
+
else:
|
|
226
|
+
surface = 'track'
|
|
227
|
+
|
|
228
|
+
## there are no trail factors, so use road factors if trail requested
|
|
229
|
+
elif surface == 'trail':
|
|
230
|
+
surface = 'road'
|
|
231
|
+
|
|
226
232
|
# check distance within range. Make min and max float so exception format specification works
|
|
227
|
-
distlist = list(self.agegradedata[gen].keys())
|
|
233
|
+
distlist = list(self.agegradedata[surface][gen].keys())
|
|
228
234
|
minmeters = min(distlist)*1.0
|
|
229
235
|
maxmeters = max(distlist)*1.0
|
|
230
|
-
|
|
236
|
+
epsilon = 1 # meter fuzziness
|
|
237
|
+
if distmeters < minmeters-epsilon or distmeters > maxmeters+epsilon:
|
|
238
|
+
if errorlogger:
|
|
239
|
+
errorlogger(f'received age={age} gen={gen} distmiles={distmiles} surface={initialsurface} time={time}, used surface={surface}')
|
|
231
240
|
raise parameterError(
|
|
232
241
|
'distmiles must be between {:0.3f} and {:0.1f}'.format(minmeters / mpermile, maxmeters / mpermile))
|
|
233
242
|
|
|
234
243
|
# interpolate factor and openstd based on distance for this age
|
|
235
244
|
age = int(age)
|
|
236
245
|
if age in range(5,100):
|
|
237
|
-
factor,openstd = self.getfactorstd(age,gen,distmeters)
|
|
246
|
+
factor,openstd = self.getfactorstd(surface, age, gen, distmeters)
|
|
238
247
|
|
|
239
248
|
# extrapolate for ages < 5
|
|
240
249
|
elif age < 5:
|
|
241
250
|
if True:
|
|
242
|
-
factor,openstd = self.getfactorstd(5,gen,distmeters)
|
|
251
|
+
factor,openstd = self.getfactorstd(surface, 5, gen, distmeters)
|
|
243
252
|
|
|
244
253
|
# don't do extrapolation
|
|
245
254
|
else:
|
|
@@ -253,7 +262,7 @@ class AgeGrade():
|
|
|
253
262
|
# extrapolate for ages > 99
|
|
254
263
|
elif age > 99:
|
|
255
264
|
if True:
|
|
256
|
-
factor,openstd = self.getfactorstd(99,gen,distmeters)
|
|
265
|
+
factor,openstd = self.getfactorstd(surface, 99, gen, distmeters)
|
|
257
266
|
|
|
258
267
|
# don't do extrapolation
|
|
259
268
|
else:
|
|
@@ -268,18 +277,16 @@ class AgeGrade():
|
|
|
268
277
|
agpercentage = 100*(openstd/factor)/time
|
|
269
278
|
agresult = time*factor
|
|
270
279
|
if self.DEBUG:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return agpercentage,agresult,factor
|
|
280
|
+
self.DEBUG(f'dist={distmeters} surf={surface} age={age} gen={gen} openstd={openstd} factor={factor} time={time} agresult={agresult} ag%={agpercentage}\n')
|
|
281
|
+
return agpercentage, agresult, factor
|
|
274
282
|
|
|
275
|
-
|
|
276
|
-
def result(self,age,gen,distmiles,agpc):
|
|
277
|
-
#----------------------------------------------------------------------
|
|
283
|
+
def result(self,surface, age, gen, distmiles, agpc):
|
|
278
284
|
'''
|
|
279
285
|
returns age grade statistics for the indicated age, gender, distance, result time
|
|
280
286
|
|
|
281
287
|
NOTE: non-binary gen X currently returns Men's age grade
|
|
282
288
|
|
|
289
|
+
:param surface: 'road' or 'track'
|
|
283
290
|
:param age: integer age. If float is supplied, integer portion is used (no interpolation of fractional age)
|
|
284
291
|
:param gen: gender - M, F, X
|
|
285
292
|
:param distmiles: distance (miles)
|
|
@@ -306,7 +313,7 @@ class AgeGrade():
|
|
|
306
313
|
distmeters = distmiles*mpermile
|
|
307
314
|
|
|
308
315
|
# check distance within range. Make min and max float so exception format specification works
|
|
309
|
-
distlist = list(self.agegradedata[gen].keys())
|
|
316
|
+
distlist = list(self.agegradedata[surface][gen].keys())
|
|
310
317
|
minmeters = min(distlist)*1.0
|
|
311
318
|
maxmeters = max(distlist)*1.0
|
|
312
319
|
if distmeters < minmeters or distmeters > maxmeters:
|
|
@@ -349,9 +356,7 @@ class AgeGrade():
|
|
|
349
356
|
time = (openstd/factor)/(agpc/100.0)
|
|
350
357
|
return time
|
|
351
358
|
|
|
352
|
-
#----------------------------------------------------------------------
|
|
353
359
|
def main():
|
|
354
|
-
#----------------------------------------------------------------------
|
|
355
360
|
descr = '''
|
|
356
361
|
Update configuration for agegrade.py. One of --agworkbook or --agconfigfile must be used,
|
|
357
362
|
but not both.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
loutilities/agegrade.py
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/python
|
|
2
|
-
###########################################################################################
|
|
3
|
-
# agegrade - calculate age grade statistics
|
|
4
|
-
#
|
|
5
|
-
# Date Author Reason
|
|
6
|
-
# ---- ------ ------
|
|
7
|
-
# 02/17/13 Lou King Create
|
|
8
|
-
# 03/20/14 Lou King Moved from runningclub
|
|
9
|
-
#
|
|
10
|
-
# Copyright 2013,2014 Lou King
|
|
11
|
-
#
|
|
12
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
-
# you may not use this file except in compliance with the License.
|
|
14
|
-
# You may obtain a copy of the License at
|
|
15
|
-
#
|
|
16
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
-
#
|
|
18
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
-
# See the License for the specific language governing permissions and
|
|
22
|
-
# limitations under the License.
|
|
23
|
-
#
|
|
24
|
-
###########################################################################################
|
|
25
1
|
'''
|
|
26
2
|
agegrade - calculate age grade statistics
|
|
27
3
|
===================================================
|
|
@@ -43,9 +19,7 @@ import shutil
|
|
|
43
19
|
class missingConfiguration(Exception): pass
|
|
44
20
|
class parameterError(Exception): pass
|
|
45
21
|
|
|
46
|
-
#----------------------------------------------------------------------
|
|
47
22
|
def getagtable(agegradewb):
|
|
48
|
-
#----------------------------------------------------------------------
|
|
49
23
|
'''
|
|
50
24
|
in return data structure:
|
|
51
25
|
|
|
@@ -56,10 +30,25 @@ def getagtable(agegradewb):
|
|
|
56
30
|
|
|
57
31
|
:param agegradewb: excel workbook containing age grade factors (e.g., from http://www.howardgrubb.co.uk/athletics/data/wavacalc10.xls)
|
|
58
32
|
|
|
59
|
-
:rtype:
|
|
33
|
+
:rtype: dict: {
|
|
34
|
+
'road: {'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},'M':{dist:{'OC':openstd,age:factor,age:factor,...},...}},
|
|
35
|
+
'trail: {'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},'M':{dist:{'OC':openstd,age:factor,age:factor,...},...}},
|
|
36
|
+
}
|
|
37
|
+
|
|
60
38
|
'''
|
|
61
39
|
|
|
62
|
-
agegradedata = {
|
|
40
|
+
agegradedata = {
|
|
41
|
+
'road': {
|
|
42
|
+
'F': {},
|
|
43
|
+
'M': {},
|
|
44
|
+
'X': {},
|
|
45
|
+
},
|
|
46
|
+
'track': {
|
|
47
|
+
'F': {},
|
|
48
|
+
'M': {},
|
|
49
|
+
'X': {},
|
|
50
|
+
},
|
|
51
|
+
}
|
|
63
52
|
|
|
64
53
|
# convert the workbook to csv
|
|
65
54
|
from .csvwt import Xls2Csv
|
|
@@ -82,9 +71,6 @@ def getagtable(agegradewb):
|
|
|
82
71
|
except ValueError:
|
|
83
72
|
pass
|
|
84
73
|
|
|
85
|
-
# create gender
|
|
86
|
-
agegradedata[gen] = {}
|
|
87
|
-
|
|
88
74
|
# add each row to data structure, but skip non-running events
|
|
89
75
|
for r in sheet:
|
|
90
76
|
if r['dist(km)'] == '0.0': continue
|
|
@@ -92,20 +78,16 @@ def getagtable(agegradewb):
|
|
|
92
78
|
# dist is rounded to the nearest meter so it can be used as a key
|
|
93
79
|
dist = int(round(float(r['dist(km)'])*1000))
|
|
94
80
|
|
|
95
|
-
# kludge to use only road events -- affects distances 5km and beyond (issue #55)
|
|
96
|
-
# this works because Howard Grubb's spreadsheet has road first, then track for events which matter
|
|
97
|
-
# as of wavacalc10.xls, includes 5k, 6k, 4M, 8k, 5M, 10k distances
|
|
98
|
-
if dist in agegradedata[gen]: continue
|
|
99
|
-
|
|
100
81
|
# create dist
|
|
82
|
+
surface = 'road' if r['isRoad'] == 1 else 'track'
|
|
101
83
|
openstd = float(r['OC'])
|
|
102
|
-
agegradedata[gen][dist] = {'OC':openstd}
|
|
84
|
+
agegradedata[surface][gen][dist] = {'OC':openstd}
|
|
103
85
|
|
|
104
86
|
# add each age factor
|
|
105
87
|
for f in sheet.fieldnames:
|
|
106
88
|
if f in f2age:
|
|
107
89
|
age = f2age[f]
|
|
108
|
-
agegradedata[gen][dist][age] = float(r[f])
|
|
90
|
+
agegradedata[surface][gen][dist][age] = float(r[f])
|
|
109
91
|
|
|
110
92
|
|
|
111
93
|
SHEET.close()
|
|
@@ -113,31 +95,41 @@ def getagtable(agegradewb):
|
|
|
113
95
|
del c
|
|
114
96
|
return agegradedata
|
|
115
97
|
|
|
116
|
-
|
|
98
|
+
|
|
117
99
|
class AgeGrade():
|
|
118
|
-
########################################################################
|
|
119
100
|
'''
|
|
120
101
|
AgeGrade object
|
|
121
102
|
|
|
122
|
-
agegradewb is in format per http://www.howardgrubb.co.uk/athletics/
|
|
123
|
-
if agegradewb parameter is missing, previous configuration is used
|
|
124
|
-
configuration is created through command line: agegrade.py [-a agworkbook | -c agconfigfile]
|
|
103
|
+
agegradewb is in format per http://www.howardgrubb.co.uk/athletics/wmalookup15.html (deprecated)
|
|
104
|
+
if agegradewb parameter is missing, previous configuration is used (deprecated)
|
|
105
|
+
configuration is created through command line: agegrade.py [-a agworkbook | -c agconfigfile] (deprecated)
|
|
125
106
|
|
|
126
|
-
:param
|
|
127
|
-
|
|
107
|
+
:param agegradedata: data structure used by this class
|
|
108
|
+
{
|
|
109
|
+
'road: {
|
|
110
|
+
'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
111
|
+
'M':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
112
|
+
'X':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
113
|
+
},
|
|
114
|
+
'trail: {
|
|
115
|
+
'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
116
|
+
'M':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
117
|
+
'X':{dist:{'OC':openstd,age:factor,age:factor,...},...},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
:param agegradewb: (deprecated) excel workbook containing age grade factors
|
|
121
|
+
:param DEBUG: logger function for debug output
|
|
128
122
|
'''
|
|
129
|
-
|
|
130
|
-
def __init__(self,agegradewb=None,DEBUG=None):
|
|
131
|
-
#----------------------------------------------------------------------
|
|
123
|
+
def __init__(self, agegradedata=None, agegradewb=None, DEBUG=None):
|
|
132
124
|
from .config import CONFIGDIR
|
|
133
125
|
self.DEBUG = DEBUG
|
|
134
126
|
|
|
135
|
-
#
|
|
136
|
-
if
|
|
137
|
-
self.
|
|
138
|
-
|
|
127
|
+
# use age grade data structure if specified
|
|
128
|
+
if agegradedata:
|
|
129
|
+
self.agegradedata = agegradedata
|
|
130
|
+
|
|
139
131
|
# use age grade workbook if specified
|
|
140
|
-
|
|
132
|
+
elif agegradewb:
|
|
141
133
|
self.agegradedata = getagtable(agegradewb)
|
|
142
134
|
|
|
143
135
|
# otherwise, pick up the data from the configuration
|
|
@@ -150,12 +142,11 @@ class AgeGrade():
|
|
|
150
142
|
self.agegradedata = pickle.load(C)
|
|
151
143
|
C.close()
|
|
152
144
|
|
|
153
|
-
|
|
154
|
-
def getfactorstd(self,age,gen,distmeters):
|
|
155
|
-
#----------------------------------------------------------------------
|
|
145
|
+
def getfactorstd(self, surface, age, gen, distmeters):
|
|
156
146
|
'''
|
|
157
147
|
interpolate factor and openstd based on distance for this age
|
|
158
148
|
|
|
149
|
+
:param surface: 'road' or 'track'
|
|
159
150
|
:param age: integer age. If float is supplied, integer portion is used (no interpolation of fractional age)
|
|
160
151
|
:param gen: gender - M or F
|
|
161
152
|
:param distmeters: distance (meters)
|
|
@@ -171,16 +162,16 @@ class AgeGrade():
|
|
|
171
162
|
distmeters = round(distmeters)
|
|
172
163
|
|
|
173
164
|
# find surrounding Xi points, and corresponding Fi, OCi points
|
|
174
|
-
distlist = sorted(list(self.agegradedata[gen].keys()))
|
|
165
|
+
distlist = sorted(list(self.agegradedata[surface][gen].keys()))
|
|
175
166
|
lastd = distlist[0]
|
|
176
167
|
for i in range(1,len(distlist)):
|
|
177
168
|
if distmeters <= distlist[i]:
|
|
178
169
|
x0 = lastd
|
|
179
170
|
x1 = distlist[i]
|
|
180
|
-
f0 = self.agegradedata[gen][x0][age]
|
|
181
|
-
f1 = self.agegradedata[gen][x1][age]
|
|
182
|
-
oc0 = self.agegradedata[gen][x0]['OC']
|
|
183
|
-
oc1 = self.agegradedata[gen][x1]['OC']
|
|
171
|
+
f0 = self.agegradedata[surface][gen][x0][age]
|
|
172
|
+
f1 = self.agegradedata[surface][gen][x1][age]
|
|
173
|
+
oc0 = self.agegradedata[surface][gen][x0]['OC']
|
|
174
|
+
oc1 = self.agegradedata[surface][gen][x1]['OC']
|
|
184
175
|
break
|
|
185
176
|
lastd = distlist[i]
|
|
186
177
|
|
|
@@ -190,9 +181,7 @@ class AgeGrade():
|
|
|
190
181
|
|
|
191
182
|
return factor,openstd
|
|
192
183
|
|
|
193
|
-
|
|
194
|
-
def agegrade(self,age,gen,distmiles,time):
|
|
195
|
-
#----------------------------------------------------------------------
|
|
184
|
+
def agegrade(self, age, gen, distmiles, time, surface=None, errorlogger=None):
|
|
196
185
|
'''
|
|
197
186
|
returns age grade statistics for the indicated age, gender, distance, result time
|
|
198
187
|
|
|
@@ -202,6 +191,7 @@ class AgeGrade():
|
|
|
202
191
|
:param gen: gender - M, F, X
|
|
203
192
|
:param distmiles: distance (miles)
|
|
204
193
|
:param time: time for distance (seconds)
|
|
194
|
+
:param surface: (optional) 'road' or 'track', default 'road'
|
|
205
195
|
|
|
206
196
|
:rtype: (age performance percentage, age graded result, age grade factor) - percentage is between 0 and 100, result is in seconds
|
|
207
197
|
'''
|
|
@@ -223,23 +213,42 @@ class AgeGrade():
|
|
|
223
213
|
else:
|
|
224
214
|
distmeters = distmiles*mpermile
|
|
225
215
|
|
|
216
|
+
# surface might require some adjustment
|
|
217
|
+
## if surface not provided, assume road if we have road factors for this distance, else assume track
|
|
218
|
+
## this maintains backwards compatibility, or for when caller has no access to what surface a race was run on
|
|
219
|
+
initialsurface = surface
|
|
220
|
+
if not surface:
|
|
221
|
+
minroad = min(list(self.agegradedata['road'][gen].keys()))
|
|
222
|
+
# need to round distmeters here because dist keys are rounded integers
|
|
223
|
+
if int(round(distmeters)) >= minroad:
|
|
224
|
+
surface = 'road'
|
|
225
|
+
else:
|
|
226
|
+
surface = 'track'
|
|
227
|
+
|
|
228
|
+
## there are no trail factors, so use road factors if trail requested
|
|
229
|
+
elif surface == 'trail':
|
|
230
|
+
surface = 'road'
|
|
231
|
+
|
|
226
232
|
# check distance within range. Make min and max float so exception format specification works
|
|
227
|
-
distlist = list(self.agegradedata[gen].keys())
|
|
233
|
+
distlist = list(self.agegradedata[surface][gen].keys())
|
|
228
234
|
minmeters = min(distlist)*1.0
|
|
229
235
|
maxmeters = max(distlist)*1.0
|
|
230
|
-
|
|
236
|
+
epsilon = 1 # meter fuzziness
|
|
237
|
+
if distmeters < minmeters-epsilon or distmeters > maxmeters+epsilon:
|
|
238
|
+
if errorlogger:
|
|
239
|
+
errorlogger(f'received age={age} gen={gen} distmiles={distmiles} surface={initialsurface} time={time}, used surface={surface}')
|
|
231
240
|
raise parameterError(
|
|
232
241
|
'distmiles must be between {:0.3f} and {:0.1f}'.format(minmeters / mpermile, maxmeters / mpermile))
|
|
233
242
|
|
|
234
243
|
# interpolate factor and openstd based on distance for this age
|
|
235
244
|
age = int(age)
|
|
236
245
|
if age in range(5,100):
|
|
237
|
-
factor,openstd = self.getfactorstd(age,gen,distmeters)
|
|
246
|
+
factor,openstd = self.getfactorstd(surface, age, gen, distmeters)
|
|
238
247
|
|
|
239
248
|
# extrapolate for ages < 5
|
|
240
249
|
elif age < 5:
|
|
241
250
|
if True:
|
|
242
|
-
factor,openstd = self.getfactorstd(5,gen,distmeters)
|
|
251
|
+
factor,openstd = self.getfactorstd(surface, 5, gen, distmeters)
|
|
243
252
|
|
|
244
253
|
# don't do extrapolation
|
|
245
254
|
else:
|
|
@@ -253,7 +262,7 @@ class AgeGrade():
|
|
|
253
262
|
# extrapolate for ages > 99
|
|
254
263
|
elif age > 99:
|
|
255
264
|
if True:
|
|
256
|
-
factor,openstd = self.getfactorstd(99,gen,distmeters)
|
|
265
|
+
factor,openstd = self.getfactorstd(surface, 99, gen, distmeters)
|
|
257
266
|
|
|
258
267
|
# don't do extrapolation
|
|
259
268
|
else:
|
|
@@ -268,18 +277,16 @@ class AgeGrade():
|
|
|
268
277
|
agpercentage = 100*(openstd/factor)/time
|
|
269
278
|
agresult = time*factor
|
|
270
279
|
if self.DEBUG:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return agpercentage,agresult,factor
|
|
280
|
+
self.DEBUG(f'dist={distmeters} surf={surface} age={age} gen={gen} openstd={openstd} factor={factor} time={time} agresult={agresult} ag%={agpercentage}\n')
|
|
281
|
+
return agpercentage, agresult, factor
|
|
274
282
|
|
|
275
|
-
|
|
276
|
-
def result(self,age,gen,distmiles,agpc):
|
|
277
|
-
#----------------------------------------------------------------------
|
|
283
|
+
def result(self,surface, age, gen, distmiles, agpc):
|
|
278
284
|
'''
|
|
279
285
|
returns age grade statistics for the indicated age, gender, distance, result time
|
|
280
286
|
|
|
281
287
|
NOTE: non-binary gen X currently returns Men's age grade
|
|
282
288
|
|
|
289
|
+
:param surface: 'road' or 'track'
|
|
283
290
|
:param age: integer age. If float is supplied, integer portion is used (no interpolation of fractional age)
|
|
284
291
|
:param gen: gender - M, F, X
|
|
285
292
|
:param distmiles: distance (miles)
|
|
@@ -306,7 +313,7 @@ class AgeGrade():
|
|
|
306
313
|
distmeters = distmiles*mpermile
|
|
307
314
|
|
|
308
315
|
# check distance within range. Make min and max float so exception format specification works
|
|
309
|
-
distlist = list(self.agegradedata[gen].keys())
|
|
316
|
+
distlist = list(self.agegradedata[surface][gen].keys())
|
|
310
317
|
minmeters = min(distlist)*1.0
|
|
311
318
|
maxmeters = max(distlist)*1.0
|
|
312
319
|
if distmeters < minmeters or distmeters > maxmeters:
|
|
@@ -349,9 +356,7 @@ class AgeGrade():
|
|
|
349
356
|
time = (openstd/factor)/(agpc/100.0)
|
|
350
357
|
return time
|
|
351
358
|
|
|
352
|
-
#----------------------------------------------------------------------
|
|
353
359
|
def main():
|
|
354
|
-
#----------------------------------------------------------------------
|
|
355
360
|
descr = '''
|
|
356
361
|
Update configuration for agegrade.py. One of --agworkbook or --agconfigfile must be used,
|
|
357
362
|
but not both.
|
loutilities/tables.py
CHANGED
|
@@ -16,6 +16,7 @@ from urllib.parse import urlencode
|
|
|
16
16
|
from threading import RLock
|
|
17
17
|
import sys
|
|
18
18
|
import json
|
|
19
|
+
from traceback import format_exception_only, format_exc
|
|
19
20
|
|
|
20
21
|
# pypi
|
|
21
22
|
import flask
|
|
@@ -3444,3 +3445,37 @@ class DteFormValidate():
|
|
|
3444
3445
|
|
|
3445
3446
|
return {'python':py, 'results':results}
|
|
3446
3447
|
|
|
3448
|
+
def apimethod(f):
|
|
3449
|
+
'''
|
|
3450
|
+
decorator for api methods
|
|
3451
|
+
|
|
3452
|
+
:param f: function() containing core of method to execute
|
|
3453
|
+
'''
|
|
3454
|
+
# see http://python-3-patterns-idioms-test.readthedocs.io/en/latest/PythonDecorators.html
|
|
3455
|
+
def wrapped_f(self, *args, **kwargs):
|
|
3456
|
+
try:
|
|
3457
|
+
# verify user can write the data, otherwise abort
|
|
3458
|
+
if not self.permission():
|
|
3459
|
+
self.rollback()
|
|
3460
|
+
cause = 'operation not permitted for user'
|
|
3461
|
+
return jsonify(error=cause)
|
|
3462
|
+
|
|
3463
|
+
# execute core of method
|
|
3464
|
+
return f(self, *args, **kwargs)
|
|
3465
|
+
|
|
3466
|
+
except Exception as e:
|
|
3467
|
+
output_result = {'status': 'fail'}
|
|
3468
|
+
# abort if errors seen
|
|
3469
|
+
if hasattr(self, '_fielderrors') and self._fielderrors:
|
|
3470
|
+
cause = 'please check indicated fields'
|
|
3471
|
+
output_result.update({'error': cause, 'fieldErrors': self._fielderrors})
|
|
3472
|
+
else:
|
|
3473
|
+
exc = ''.join(format_exception_only(type(e), e))
|
|
3474
|
+
output_result['error'] = 'exception occurred:\n{}'.format(exc)
|
|
3475
|
+
current_app.logger.error(format_exc())
|
|
3476
|
+
|
|
3477
|
+
# roll back database updates and close transaction
|
|
3478
|
+
self.rollback()
|
|
3479
|
+
return jsonify(output_result)
|
|
3480
|
+
return wrapped_f
|
|
3481
|
+
|
loutilities/user/__init__.py
CHANGED
|
@@ -22,9 +22,9 @@ user_messages = {
|
|
|
22
22
|
|
|
23
23
|
# login_form for application management
|
|
24
24
|
class UserLoginForm(LoginForm):
|
|
25
|
-
def validate(self):
|
|
25
|
+
def validate(self, **kwargs):
|
|
26
26
|
# if some error was detected from standard validate(), we're done
|
|
27
|
-
if not super().validate():
|
|
27
|
+
if not super().validate(**kwargs):
|
|
28
28
|
return False
|
|
29
29
|
|
|
30
30
|
# if all ok otherwise, check roles to verify user allowed for this application
|
|
@@ -41,9 +41,9 @@ class UserLoginForm(LoginForm):
|
|
|
41
41
|
|
|
42
42
|
# forgot_password for application management
|
|
43
43
|
class UserForgotPasswordForm(ForgotPasswordForm):
|
|
44
|
-
def validate(self):
|
|
44
|
+
def validate(self, **kwargs):
|
|
45
45
|
# if some error was detected from standard validate(), we're done
|
|
46
|
-
if not super().validate():
|
|
46
|
+
if not super().validate(**kwargs):
|
|
47
47
|
return False
|
|
48
48
|
|
|
49
49
|
# if all ok otherwise, check roles to verify user allowed for this application
|
|
@@ -51,7 +51,7 @@ class UserForgotPasswordForm(ForgotPasswordForm):
|
|
|
51
51
|
apps = set()
|
|
52
52
|
for thisrole in self.user.roles:
|
|
53
53
|
apps |= set(thisrole.applications)
|
|
54
|
-
## disallow
|
|
54
|
+
## disallow password reset if this app isn't in one of user's roles
|
|
55
55
|
if g.loutility not in apps:
|
|
56
56
|
self.email.errors.append(user_messages['ACCOUNT_NOT_PERMITTED'])
|
|
57
57
|
return False
|
|
Binary file
|
|
Binary file
|
loutilities/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# this string is used for the version string in the documentation, as well as the egg
|
|
2
|
-
__version__ = '3.
|
|
2
|
+
__version__ = '3.8.1'
|