geo-activity-playground 0.20.0__tar.gz → 0.21.0__tar.gz
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.
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/PKG-INFO +1 -2
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/__main__.py +16 -3
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/activities.py +47 -29
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/activity_parsers.py +4 -0
- geo_activity_playground-0.21.0/geo_activity_playground/importers/directory.py +127 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/importers/strava_checkout.py +10 -1
- geo_activity_playground-0.21.0/geo_activity_playground/importers/test_directory.py +22 -0
- geo_activity_playground-0.21.0/geo_activity_playground/webui/activity_controller.py +363 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/app.py +30 -3
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/calendar_controller.py +8 -4
- geo_activity_playground-0.21.0/geo_activity_playground/webui/templates/activity-day.html.j2 +71 -0
- geo_activity_playground-0.21.0/geo_activity_playground/webui/templates/activity-lines.html.j2 +36 -0
- geo_activity_playground-0.21.0/geo_activity_playground/webui/templates/activity-name.html.j2 +81 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/activity.html.j2 +36 -12
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/calendar-month.html.j2 +7 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/tile_controller.py +10 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/pyproject.toml +1 -2
- geo_activity_playground-0.20.0/geo_activity_playground/importers/directory.py +0 -103
- geo_activity_playground-0.20.0/geo_activity_playground/webui/activity_controller.py +0 -195
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/LICENSE +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/__init__.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/__init__.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/cache_migrations.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/config.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/coordinates.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/heatmap.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/paths.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/similarity.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/tasks.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/test_tiles.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/core/tiles.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/explorer/__init__.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/explorer/grid_file.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/explorer/tile_visits.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/explorer/video.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/importers/strava_api.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/config_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/eddington_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/entry_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/equipment_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/explorer_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/heatmap_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/locations_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/search_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/square_planner_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/favicon.ico +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/static/site.webmanifest +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/strava_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/summary_controller.py +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/calendar.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/config.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/eddington.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/equipment.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/explorer.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/heatmap.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/index.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/locations.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/page.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/search.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/square-planner.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/strava-connect.html.j2 +0 -0
- {geo_activity_playground-0.20.0 → geo_activity_playground-0.21.0}/geo_activity_playground/webui/templates/summary.html.j2 +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: geo-activity-playground
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.21.0
|
4
4
|
Summary: Analysis of geo data activities like rides, runs or hikes.
|
5
5
|
License: MIT
|
6
6
|
Author: Martin Ueding
|
@@ -20,7 +20,6 @@ Requires-Dist: fitdecode (>=0.10.0,<0.11.0)
|
|
20
20
|
Requires-Dist: flask (>=3.0.0,<4.0.0)
|
21
21
|
Requires-Dist: geojson (>=3.0.1,<4.0.0)
|
22
22
|
Requires-Dist: gpxpy (>=1.5.0,<2.0.0)
|
23
|
-
Requires-Dist: imagehash (>=4.3.1,<5.0.0)
|
24
23
|
Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
|
25
24
|
Requires-Dist: matplotlib (>=3.6.3,<4.0.0)
|
26
25
|
Requires-Dist: numpy (>=1.22.4,<2.0.0)
|
@@ -6,7 +6,6 @@ import sys
|
|
6
6
|
|
7
7
|
import coloredlogs
|
8
8
|
|
9
|
-
from .core.similarity import precompute_activity_distances
|
10
9
|
from .importers.strava_checkout import convert_strava_checkout
|
11
10
|
from .importers.strava_checkout import import_from_strava_checkout
|
12
11
|
from geo_activity_playground.core.activities import ActivityRepository
|
@@ -105,17 +104,31 @@ def make_activity_repository(
|
|
105
104
|
apply_cache_migrations()
|
106
105
|
config = get_config()
|
107
106
|
|
107
|
+
if not config.get("prefer_metadata_from_file", True):
|
108
|
+
logger.error(
|
109
|
+
"The config option `prefer_metadata_from_file` is deprecated. If you want to prefer extract metadata from the activity file paths, please use the new `metadata_extraction_regexes` as explained at https://martin-ueding.github.io/geo-activity-playground/getting-started/using-activity-files/#directory-structure."
|
110
|
+
)
|
111
|
+
sys.exit(1)
|
112
|
+
|
108
113
|
repository = ActivityRepository()
|
109
114
|
|
110
115
|
if pathlib.Path("Activities").exists():
|
111
|
-
import_from_directory(
|
116
|
+
import_from_directory(
|
117
|
+
repository,
|
118
|
+
config.get("metadata_extraction_regexes", []),
|
119
|
+
)
|
112
120
|
if pathlib.Path("Strava Export").exists():
|
113
121
|
import_from_strava_checkout(repository)
|
114
122
|
if "strava" in config and not skip_strava:
|
115
123
|
import_from_strava_api(repository)
|
116
124
|
|
125
|
+
if len(repository) == 0:
|
126
|
+
logger.error(
|
127
|
+
f"No activities found. You need to either add activity files (GPX, FIT, …) to {basedir/'Activities'} or set up the Strava API. Starting without any activities is unfortunately not supported."
|
128
|
+
)
|
129
|
+
sys.exit(1)
|
130
|
+
|
117
131
|
embellish_time_series(repository)
|
118
|
-
precompute_activity_distances(repository)
|
119
132
|
compute_tile_visits(repository)
|
120
133
|
compute_tile_evolution()
|
121
134
|
return repository
|
@@ -37,6 +37,7 @@ class ActivityMeta(TypedDict):
|
|
37
37
|
start_latitude: float
|
38
38
|
start_longitude: float
|
39
39
|
start: datetime.datetime
|
40
|
+
steps: int
|
40
41
|
|
41
42
|
|
42
43
|
class ActivityRepository:
|
@@ -163,9 +164,6 @@ def embellish_single_time_series(
|
|
163
164
|
timeseries: pd.DataFrame, start: Optional[datetime.datetime] = None
|
164
165
|
) -> bool:
|
165
166
|
changed = False
|
166
|
-
time_diff_threshold_seconds = 30
|
167
|
-
time_diff = (timeseries["time"] - timeseries["time"].shift(1)).dt.total_seconds()
|
168
|
-
jump_indices = time_diff >= time_diff_threshold_seconds
|
169
167
|
|
170
168
|
if start is not None and pd.api.types.is_dtype_equal(
|
171
169
|
timeseries["time"].dtype, "int64"
|
@@ -176,31 +174,37 @@ def embellish_single_time_series(
|
|
176
174
|
changed = True
|
177
175
|
assert pd.api.types.is_dtype_equal(timeseries["time"].dtype, "datetime64[ns, UTC]")
|
178
176
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
177
|
+
distances = get_distance(
|
178
|
+
timeseries["latitude"].shift(1),
|
179
|
+
timeseries["longitude"].shift(1),
|
180
|
+
timeseries["latitude"],
|
181
|
+
timeseries["longitude"],
|
182
|
+
).fillna(0.0)
|
183
|
+
time_diff_threshold_seconds = 30
|
184
|
+
time_diff = (timeseries["time"] - timeseries["time"].shift(1)).dt.total_seconds()
|
185
|
+
jump_indices = (time_diff >= time_diff_threshold_seconds) & (distances > 100)
|
186
|
+
distances.loc[jump_indices] = 0.0
|
187
|
+
|
188
|
+
if not "distance_km" in timeseries.columns:
|
188
189
|
timeseries["distance_km"] = pd.Series(np.cumsum(distances)) / 1000
|
189
190
|
changed = True
|
190
191
|
|
191
|
-
if "
|
192
|
-
|
193
|
-
timeseries["
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
changed = True
|
192
|
+
if "speed" not in timeseries.columns:
|
193
|
+
timeseries["speed"] = (
|
194
|
+
timeseries["distance_km"].diff()
|
195
|
+
/ (timeseries["time"].diff().dt.total_seconds() + 1e-3)
|
196
|
+
* 3600
|
197
|
+
)
|
198
|
+
changed = True
|
199
199
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
200
|
+
potential_jumps = (timeseries["speed"] > 40) & (timeseries["speed"].diff() > 10)
|
201
|
+
if np.any(potential_jumps):
|
202
|
+
timeseries = timeseries.loc[~potential_jumps].copy()
|
203
|
+
changed = True
|
204
|
+
|
205
|
+
if "segment_id" not in timeseries.columns:
|
206
|
+
timeseries["segment_id"] = np.cumsum(jump_indices)
|
207
|
+
changed = True
|
204
208
|
|
205
209
|
if "x" not in timeseries.columns:
|
206
210
|
x, y = compute_tile_float(timeseries["latitude"], timeseries["longitude"], 0)
|
@@ -208,10 +212,6 @@ def embellish_single_time_series(
|
|
208
212
|
timeseries["y"] = y
|
209
213
|
changed = True
|
210
214
|
|
211
|
-
if "segment_id" not in timeseries.columns:
|
212
|
-
timeseries["segment_id"] = np.cumsum(jump_indices)
|
213
|
-
changed = True
|
214
|
-
|
215
215
|
return timeseries, changed
|
216
216
|
|
217
217
|
|
@@ -228,6 +228,11 @@ def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
|
|
228
228
|
|
229
229
|
|
230
230
|
def make_geojson_color_line(time_series: pd.DataFrame) -> str:
|
231
|
+
speed_without_na = time_series["speed"].dropna()
|
232
|
+
low = min(speed_without_na)
|
233
|
+
high = max(speed_without_na)
|
234
|
+
clamp_speed = lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
|
235
|
+
|
231
236
|
cmap = matplotlib.colormaps["viridis"]
|
232
237
|
features = [
|
233
238
|
geojson.Feature(
|
@@ -239,7 +244,7 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
|
|
239
244
|
),
|
240
245
|
properties={
|
241
246
|
"speed": next["speed"] if np.isfinite(next["speed"]) else 0.0,
|
242
|
-
"color": matplotlib.colors.to_hex(cmap(
|
247
|
+
"color": matplotlib.colors.to_hex(cmap(clamp_speed(next["speed"]))),
|
243
248
|
},
|
244
249
|
)
|
245
250
|
for _, group in time_series.groupby("segment_id")
|
@@ -249,6 +254,19 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
|
|
249
254
|
return geojson.dumps(feature_collection)
|
250
255
|
|
251
256
|
|
257
|
+
def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, str]:
|
258
|
+
speed_without_na = time_series["speed"].dropna()
|
259
|
+
low = min(speed_without_na)
|
260
|
+
high = max(speed_without_na)
|
261
|
+
cmap = matplotlib.colormaps["viridis"]
|
262
|
+
clamp_speed = lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
|
263
|
+
colors = [
|
264
|
+
(f"{speed:.1f}", matplotlib.colors.to_hex(cmap(clamp_speed(speed))))
|
265
|
+
for speed in np.linspace(low, high, 10)
|
266
|
+
]
|
267
|
+
return {"low": low, "high": high, "colors": colors}
|
268
|
+
|
269
|
+
|
252
270
|
def extract_heart_rate_zones(time_series: pd.DataFrame) -> Optional[pd.DataFrame]:
|
253
271
|
if "heartrate" not in time_series:
|
254
272
|
return None
|
@@ -168,6 +168,10 @@ def read_fit_activity(path: pathlib.Path, open) -> tuple[ActivityMeta, pd.DataFr
|
|
168
168
|
metadata["kind"] = str(values["sport"])
|
169
169
|
if "sub_sport" in values:
|
170
170
|
metadata["kind"] += " " + str(values["sub_sport"])
|
171
|
+
if "total_calories" in fields:
|
172
|
+
metadata["calories"] = values["total_calories"]
|
173
|
+
if "total_strides" in fields:
|
174
|
+
metadata["steps"] = 2 * int(values["total_strides"])
|
171
175
|
|
172
176
|
return metadata, pd.DataFrame(rows)
|
173
177
|
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import hashlib
|
2
|
+
import logging
|
3
|
+
import multiprocessing
|
4
|
+
import pathlib
|
5
|
+
import pickle
|
6
|
+
import re
|
7
|
+
import sys
|
8
|
+
import traceback
|
9
|
+
from typing import Optional
|
10
|
+
|
11
|
+
import pandas as pd
|
12
|
+
from tqdm import tqdm
|
13
|
+
|
14
|
+
from geo_activity_playground.core.activities import ActivityMeta
|
15
|
+
from geo_activity_playground.core.activities import ActivityRepository
|
16
|
+
from geo_activity_playground.core.activity_parsers import ActivityParseError
|
17
|
+
from geo_activity_playground.core.activity_parsers import read_activity
|
18
|
+
from geo_activity_playground.core.tasks import WorkTracker
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
ACTIVITY_DIR = pathlib.Path("Activities")
|
23
|
+
|
24
|
+
|
25
|
+
def import_from_directory(
|
26
|
+
repository: ActivityRepository, metadata_extraction_regexes: list[str] = []
|
27
|
+
) -> None:
|
28
|
+
paths_with_errors = []
|
29
|
+
work_tracker = WorkTracker("parse-activity-files")
|
30
|
+
|
31
|
+
activity_paths = [
|
32
|
+
path
|
33
|
+
for path in ACTIVITY_DIR.rglob("*.*")
|
34
|
+
if path.is_file() and path.suffixes and not path.stem.startswith(".")
|
35
|
+
]
|
36
|
+
new_activity_paths = work_tracker.filter(activity_paths)
|
37
|
+
|
38
|
+
activity_stream_dir = pathlib.Path("Cache/Activity Timeseries")
|
39
|
+
activity_stream_dir.mkdir(exist_ok=True, parents=True)
|
40
|
+
file_metadata_dir = pathlib.Path("Cache/Activity Metadata")
|
41
|
+
file_metadata_dir.mkdir(exist_ok=True, parents=True)
|
42
|
+
|
43
|
+
with multiprocessing.Pool() as pool:
|
44
|
+
paths_with_errors = tqdm(
|
45
|
+
pool.imap(_cache_single_file, new_activity_paths),
|
46
|
+
desc="Parse activity metadata",
|
47
|
+
total=len(new_activity_paths),
|
48
|
+
)
|
49
|
+
paths_with_errors = [error for error in paths_with_errors if error]
|
50
|
+
|
51
|
+
for path in tqdm(new_activity_paths, desc="Collate activity metadata"):
|
52
|
+
activity_id = _get_file_hash(path)
|
53
|
+
file_metadata_path = file_metadata_dir / f"{activity_id}.pickle"
|
54
|
+
work_tracker.mark_done(activity_id)
|
55
|
+
|
56
|
+
if not file_metadata_path.exists():
|
57
|
+
continue
|
58
|
+
|
59
|
+
with open(file_metadata_path, "rb") as f:
|
60
|
+
activity_meta_from_file = pickle.load(f)
|
61
|
+
|
62
|
+
activity_meta = ActivityMeta(
|
63
|
+
id=activity_id,
|
64
|
+
# https://stackoverflow.com/a/74718395/653152
|
65
|
+
name=path.name.removesuffix("".join(path.suffixes)),
|
66
|
+
path=str(path),
|
67
|
+
kind="Unknown",
|
68
|
+
equipment="Unknown",
|
69
|
+
)
|
70
|
+
activity_meta.update(activity_meta_from_file)
|
71
|
+
activity_meta.update(_get_metadata_from_path(path, metadata_extraction_regexes))
|
72
|
+
repository.add_activity(activity_meta)
|
73
|
+
|
74
|
+
if paths_with_errors:
|
75
|
+
logger.warning(
|
76
|
+
"There were errors while parsing some of the files. These were skipped and tried again next time."
|
77
|
+
)
|
78
|
+
for path, error in paths_with_errors:
|
79
|
+
logger.error(f"{path}: {error}")
|
80
|
+
|
81
|
+
repository.commit()
|
82
|
+
|
83
|
+
work_tracker.close()
|
84
|
+
|
85
|
+
|
86
|
+
def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]:
|
87
|
+
activity_stream_dir = pathlib.Path("Cache/Activity Timeseries")
|
88
|
+
file_metadata_dir = pathlib.Path("Cache/Activity Metadata")
|
89
|
+
|
90
|
+
activity_id = _get_file_hash(path)
|
91
|
+
timeseries_path = activity_stream_dir / f"{activity_id}.parquet"
|
92
|
+
file_metadata_path = file_metadata_dir / f"{activity_id}.pickle"
|
93
|
+
|
94
|
+
if not timeseries_path.exists():
|
95
|
+
try:
|
96
|
+
activity_meta_from_file, timeseries = read_activity(path)
|
97
|
+
except ActivityParseError as e:
|
98
|
+
logger.error(f"Error while parsing file {path}:")
|
99
|
+
traceback.print_exc()
|
100
|
+
return (path, str(e))
|
101
|
+
except:
|
102
|
+
logger.error(f"Encountered a problem with {path=}, see details below.")
|
103
|
+
raise
|
104
|
+
|
105
|
+
if len(timeseries) == 0:
|
106
|
+
return
|
107
|
+
|
108
|
+
timeseries.to_parquet(timeseries_path)
|
109
|
+
with open(file_metadata_path, "wb") as f:
|
110
|
+
pickle.dump(activity_meta_from_file, f)
|
111
|
+
|
112
|
+
|
113
|
+
def _get_file_hash(path: pathlib.Path) -> int:
|
114
|
+
file_hash = hashlib.blake2s()
|
115
|
+
with open(path, "rb") as f:
|
116
|
+
while chunk := f.read(8192):
|
117
|
+
file_hash.update(chunk)
|
118
|
+
return int(file_hash.hexdigest(), 16) % 2**62
|
119
|
+
|
120
|
+
|
121
|
+
def _get_metadata_from_path(
|
122
|
+
path: pathlib.Path, metadata_extraction_regexes: list[str]
|
123
|
+
) -> dict[str, str]:
|
124
|
+
for regex in metadata_extraction_regexes:
|
125
|
+
if m := re.search(regex, str(path.relative_to(ACTIVITY_DIR))):
|
126
|
+
return m.groupdict()
|
127
|
+
return {}
|
@@ -3,6 +3,8 @@ import logging
|
|
3
3
|
import pathlib
|
4
4
|
import shutil
|
5
5
|
import traceback
|
6
|
+
from typing import Optional
|
7
|
+
from typing import Union
|
6
8
|
|
7
9
|
import dateutil.parser
|
8
10
|
import numpy as np
|
@@ -116,6 +118,13 @@ EXPECTED_COLUMNS = [
|
|
116
118
|
]
|
117
119
|
|
118
120
|
|
121
|
+
def float_or_none(x: Union[float, str]) -> Optional[float]:
|
122
|
+
try:
|
123
|
+
return float(x)
|
124
|
+
except ValueError:
|
125
|
+
return None
|
126
|
+
|
127
|
+
|
119
128
|
def import_from_strava_checkout(repository: ActivityRepository) -> None:
|
120
129
|
checkout_path = pathlib.Path("Strava Export")
|
121
130
|
activities = pd.read_csv(checkout_path / "activities.csv")
|
@@ -143,7 +152,7 @@ def import_from_strava_checkout(repository: ActivityRepository) -> None:
|
|
143
152
|
row = activities.loc[activity_id]
|
144
153
|
activity_file = checkout_path / row["Filename"]
|
145
154
|
table_activity_meta = {
|
146
|
-
"calories": row["Calories"],
|
155
|
+
"calories": float_or_none(row["Calories"]),
|
147
156
|
"commute": row["Commute"] == "true",
|
148
157
|
"distance_km": row["Distance"],
|
149
158
|
"elapsed_time": datetime.timedelta(seconds=int(row["Elapsed Time"])),
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import pathlib
|
2
|
+
|
3
|
+
from geo_activity_playground.importers.directory import _get_metadata_from_path
|
4
|
+
|
5
|
+
|
6
|
+
def test_get_metadata_from_path() -> None:
|
7
|
+
expected = {
|
8
|
+
"kind": "Radfahrt",
|
9
|
+
"equipment": "Bike 2019",
|
10
|
+
"name": "Foo-Bar to Baz24",
|
11
|
+
}
|
12
|
+
actual = _get_metadata_from_path(
|
13
|
+
pathlib.Path(
|
14
|
+
"Activities/Radfahrt/Bike 2019/Foo-Bar to Baz24/2024-03-03-17-42-10 Something something.fit"
|
15
|
+
),
|
16
|
+
[
|
17
|
+
r"(?P<kind>[^/]+)/(?P<equipment>[^/]+)/(?P<name>[^/]+)/",
|
18
|
+
r"(?P<kind>[^/]+)/(?P<equipment>[^/]+)/[-\d_ ]+(?P<name>[^/\.]+)(?:\.\w+)+$",
|
19
|
+
r"(?P<kind>[^/]+)/[-\d_ ]+(?P<name>[^/\.]+)(?:\.\w+)+$",
|
20
|
+
],
|
21
|
+
)
|
22
|
+
assert actual == expected
|