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 CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: loutilities
3
- Version: 3.7.2
3
+ Version: 3.8.1
4
4
  Summary: some hopefully useful utilities
5
5
  Home-page: http://github.com/louking/loutilities
6
6
  Author: Lou King
@@ -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: {'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},'M':{dist:{'OC':openstd,age:factor,age:factor,...},...}}
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/wmalookup06.html
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 agegradewb: excel workbook containing age grade factors
127
- :param DEBUG: file handle for debug output
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
- # write header for csv file. Must match order within self.agegrade if self.DEBUG statement
136
- if self.DEBUG:
137
- self.DEBUG.write('distmeters,age,gen,openstd,factor,time,agresult,agpercentage\n')
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
- if agegradewb:
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
- if distmeters < minmeters or distmeters > maxmeters:
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
- # order must match header written in self.__init__
272
- self.DEBUG.write('{},{},{},{},{},{},{},{}\n'.format(distmeters,age,gen,openstd,factor,time,agresult,agpercentage))
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/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: {'F':{dist:{'OC':openstd,age:factor,age:factor,...},...},'M':{dist:{'OC':openstd,age:factor,age:factor,...},...}}
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/wmalookup06.html
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 agegradewb: excel workbook containing age grade factors
127
- :param DEBUG: file handle for debug output
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
- # write header for csv file. Must match order within self.agegrade if self.DEBUG statement
136
- if self.DEBUG:
137
- self.DEBUG.write('distmeters,age,gen,openstd,factor,time,agresult,agpercentage\n')
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
- if agegradewb:
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
- if distmeters < minmeters or distmeters > maxmeters:
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
- # order must match header written in self.__init__
272
- self.DEBUG.write('{},{},{},{},{},{},{},{}\n'.format(distmeters,age,gen,openstd,factor,time,agresult,agpercentage))
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
+
@@ -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 login if this app isn't in one of user's roles
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
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.7.2'
2
+ __version__ = '3.8.1'