-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathflyby.py
287 lines (210 loc) · 6.87 KB
/
flyby.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
"""
1. Get IDs from flyby:
- distance
- elapsedTime
- startTime
>>> fb = flyby(activity_id=12345)
"""
import os
import requests
import json
import pandas as pd
from loguru import logger
def flyby(activity_id):
"""Find flybys for a given activity
Parameters
----------
activity_id : int
Strava activity id
Returns
-------
obj : Flyby
"""
return Flyby(_request_strava_flyby(activity_id))
def _request_strava_flyby(activity_id):
"""Request the Strava Labs Flyby and return the json content
Returns:
--------
dict with keys 'activity', 'matches', 'athletes'
"""
__base_url = 'https://nene.strava.com/flyby/matches/'
__headers = {'referer': 'https://labs.strava.com/flyby/viewer/',
'origin': 'https://labs.strava.com'}
logger.info("Requesting Strava Flyby for activity ID = {}".format(activity_id))
r = requests.get('{}{}'.format(__base_url, activity_id), headers=__headers)
if r.ok:
content = json.loads(r.text)
return content
else:
raise ConnectionError("Flyby returned {}".format(r.status_code))
class Flyby():
"""Results of Flyby search
Attributes
----------
raw_content: dict
Deserialized and unprocessed JSON reponse of the Strava Flyby API
"""
def __init__(self, content=None):
"""Create Flyby object
This objects serves as a convenience wrapper
around the Strava Flyby API JSON response. The easiest way to initialite is
to call the factory function flyby(activity_id).
Parameters
----------
raw_content : dict
Deserialized and unprocessed JSON response of the Strava Flyby API
raw_activity: dict
Requesting activity
raw_matches: list
Found list of matches
matches: List
Flattened list of matches ready to use for DataFrame
"""
self.raw_content = content
def __str__(self):
return f"{self.raw_content}"
def __repr__(self):
n_matches = len(self.matches)
return f"""Flyby object with {n_matches} matches.
Attributes: ids, activity, matches, athletes
Methods: matches_to_list(), matches_to_json()"""
def matches_to_list(self, **kwargs):
"""Dump flattened matches into a list
Parameters
----------
kwargs : see to_json
Returns
-------
list
"""
return self.matches_to_json(**kwargs)
def matches_to_json(self, path_or_buf=None, **kwargs):
"""Dump flattened matches into json or a list
Parameters
----------
path_or_buf : file path, optional
default=None, which means the results will be dumped into a list of dicts
distance : number, optional
Distance in km, default=None, which means all ids are returned
tol : float from 0 to 1, optional
Tolerance +/- on `distance`, only applicable if former is not None, default=0.1
Returns
-------
None or list
"""
df = pd.DataFrame(self._flatten_matches())
rv = df[self._distance_filter(**kwargs)]
if path_or_buf:
rv.to_json(path_or_buf, orient=kwargs.get('orient', 'records'))
else:
return json.loads(rv.to_json(orient=kwargs.get('orient', 'records')))
def get_ids(self, **kwargs):
"""IDs returned by Flyby
Parameters
----------
distance : number, optional
Distance in km, default=None, which means all ids are returned
tol : float from 0 to 1, optional
Tolerance +/- on `distance`, only applicable if former is not None, default=0.1
Returns
-------
list
"""
df = pd.DataFrame(self._flatten_matches())
return df[self._distance_filter(**kwargs)]['id'].tolist()
def _distance_filter(self, **kwargs):
"""
Parameters
----------
distance : number or a tuple, optional
If number, than distance is treated as a center distance and the range
is calculated as `distance` +/- `tol`*`distance`
If tuple, than the tuple is treated as a range
tol : float, optional
Only relevant with center distance, default=0.1
Returns
-------
pd.Series
"""
df = pd.DataFrame(self._flatten_matches())
if not kwargs.get('distance', None):
return df['distance'] > -1
if (type(kwargs.get('distance')) == int) or (type(kwargs.get('distance')) == float):
rv = ((df['distance'] > (1.0 - kwargs.get('tol', 0.1)) * kwargs.get('distance') * 1000) &
(df['distance'] < (1.0 + kwargs.get('tol', 0.1)) * kwargs.get('distance') * 1000))
elif type(kwargs.get('distance')) == tuple:
rv = ((df['distance'] > kwargs.get('distance')[0] * 1000) &
(df['distance'] < kwargs.get('distance')[1] * 1000))
else:
raise ValueError("Distance must be either a number or a tuple of numbers")
return rv
def _flatten_matches(self):
"""Convert all matches into flat dict
Returns
-------
dict
With keys: ['activityType', 'athleteId', 'closestDistance', 'closestPoint',
'correlation', 'distance', 'elapsedTime', 'id', 'name', 'spatialCorrelation', 'startTime' ]
"""
rv = []
for m in self.matches:
_a = m['otherActivity']
_c = m['correlation']
_a.update(_c)
rv.append(_a)
return rv
@property
def ids(self):
"""List of found activities ids
Returns
-------
list
"""
if self.raw_content:
return self.get_ids()
else:
return None
@property
def activity(self):
"""Requesting activity
Returns
-------
dict
"""
if self.raw_content:
return self.raw_content.get('activity', None)
else:
return None
@property
def matches(self):
"""Flattend matches
Returns
-------
list of dict ready to use as input for DataFrame
"""
if self.raw_content:
return self.matches_to_list()
else:
return None
@property
def matches(self):
"""Matches returned by Flyby
Returns
-------
list
"""
if self.raw_content:
return self.raw_content.get('matches', None)
else:
return None
@property
def athletes(self):
"""Athletes returned by Flyby
Returns
-------
dict
"""
if self.raw_content:
return self.raw_content.get('athletes', None)
else:
return None