geo-activity-playground 0.30.0__py3-none-any.whl → 0.32.0__py3-none-any.whl
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/core/activities.py +11 -2
- geo_activity_playground/core/config.py +1 -0
- geo_activity_playground/core/heatmap.py +63 -0
- geo_activity_playground/core/paths.py +3 -0
- geo_activity_playground/importers/directory.py +6 -2
- geo_activity_playground/webui/activity/blueprint.py +54 -2
- geo_activity_playground/webui/activity/controller.py +47 -31
- geo_activity_playground/webui/activity/templates/activity/day.html.j2 +3 -3
- geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +42 -0
- geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -2
- geo_activity_playground/webui/activity/templates/activity/show.html.j2 +7 -5
- geo_activity_playground/webui/app.py +14 -3
- geo_activity_playground/webui/heatmap/heatmap_controller.py +8 -6
- geo_activity_playground/webui/search/templates/search/index.html.j2 +2 -2
- geo_activity_playground/webui/summary/controller.py +1 -1
- geo_activity_playground/webui/summary/templates/summary/index.html.j2 +1 -1
- geo_activity_playground/webui/templates/home.html.j2 +2 -2
- geo_activity_playground/webui/upload/controller.py +1 -2
- {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/RECORD +23 -22
- {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/WHEEL +1 -1
- {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
|
|
1
1
|
import datetime
|
2
2
|
import functools
|
3
|
+
import json
|
3
4
|
import logging
|
4
5
|
import pickle
|
5
6
|
from typing import Any
|
@@ -16,6 +17,7 @@ from tqdm import tqdm
|
|
16
17
|
from geo_activity_playground.core.paths import activities_file
|
17
18
|
from geo_activity_playground.core.paths import activity_enriched_meta_dir
|
18
19
|
from geo_activity_playground.core.paths import activity_enriched_time_series_dir
|
20
|
+
from geo_activity_playground.core.paths import activity_meta_override_dir
|
19
21
|
|
20
22
|
logger = logging.getLogger(__name__)
|
21
23
|
|
@@ -83,7 +85,12 @@ def build_activity_meta() -> None:
|
|
83
85
|
rows = []
|
84
86
|
for new_id in tqdm(new_ids, desc="Register new activities"):
|
85
87
|
with open(activity_enriched_meta_dir() / f"{new_id}.pickle", "rb") as f:
|
86
|
-
|
88
|
+
data = pickle.load(f)
|
89
|
+
override_file = activity_meta_override_dir() / f"{new_id}.json"
|
90
|
+
if override_file.exists():
|
91
|
+
with open(override_file) as f:
|
92
|
+
data.update(json.load(f))
|
93
|
+
rows.append(data)
|
87
94
|
|
88
95
|
if rows:
|
89
96
|
new_shard = pd.DataFrame(rows)
|
@@ -140,7 +147,6 @@ class ActivityRepository:
|
|
140
147
|
if not dropna or not pd.isna(row["start"]):
|
141
148
|
yield row
|
142
149
|
|
143
|
-
@functools.lru_cache()
|
144
150
|
def get_activity_by_id(self, id: int) -> ActivityMeta:
|
145
151
|
activity = self.meta.loc[id]
|
146
152
|
assert isinstance(activity["name"], str), activity["name"]
|
@@ -158,6 +164,9 @@ class ActivityRepository:
|
|
158
164
|
|
159
165
|
return df
|
160
166
|
|
167
|
+
def save(self) -> None:
|
168
|
+
self._meta.to_parquet(activities_file())
|
169
|
+
|
161
170
|
|
162
171
|
def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
|
163
172
|
fc = geojson.FeatureCollection(
|
@@ -29,6 +29,7 @@ class Config:
|
|
29
29
|
)
|
30
30
|
heart_rate_resting: int = 0
|
31
31
|
heart_rate_maximum: Optional[int] = None
|
32
|
+
ignore_suffixes: list[str] = dataclasses.field(default_factory=list)
|
32
33
|
kind_renames: dict[str, str] = dataclasses.field(default_factory=dict)
|
33
34
|
kinds_without_achievements: list[str] = dataclasses.field(default_factory=list)
|
34
35
|
metadata_extraction_regexes: list[str] = dataclasses.field(default_factory=list)
|
@@ -142,6 +142,69 @@ def build_map_from_tiles(tile_bounds: TileBounds) -> np.ndarray:
|
|
142
142
|
return background
|
143
143
|
|
144
144
|
|
145
|
+
def build_map_from_tiles_around_center(
|
146
|
+
center: tuple[float, float],
|
147
|
+
zoom: int,
|
148
|
+
target: tuple[int, int],
|
149
|
+
inner_target: tuple[int, int],
|
150
|
+
) -> np.ndarray:
|
151
|
+
background = np.zeros((target[1], target[0], 3))
|
152
|
+
|
153
|
+
# We will work with the center point and have it in terms of tiles `t` and also in terms of pixels `p`. At the start we know that the tile center must be in the middle of the image.
|
154
|
+
t = np.array(center)
|
155
|
+
p = np.array([inner_target[0] / 2, inner_target[1] / 2])
|
156
|
+
|
157
|
+
# Shift both such that they are in the top-left corner of an even tile.
|
158
|
+
t_offset = np.array([center[0] % 1, center[1] % 1])
|
159
|
+
t -= t_offset
|
160
|
+
p -= t_offset * OSM_TILE_SIZE
|
161
|
+
|
162
|
+
# Shift until we have left the image.
|
163
|
+
shift = np.ceil(p / OSM_TILE_SIZE)
|
164
|
+
p -= shift * OSM_TILE_SIZE
|
165
|
+
t -= shift
|
166
|
+
|
167
|
+
num_tiles = np.ceil(np.array(target) / OSM_TILE_SIZE) + 1
|
168
|
+
|
169
|
+
for x in range(int(t[0]), int(t[0] + num_tiles[0])):
|
170
|
+
for y in range(int(t[1]), int(t[1]) + int(num_tiles[1])):
|
171
|
+
source_x_min = 0
|
172
|
+
source_y_min = 0
|
173
|
+
source_x_max = source_x_min + OSM_TILE_SIZE
|
174
|
+
source_y_max = source_y_min + OSM_TILE_SIZE
|
175
|
+
|
176
|
+
target_x_min = (x - int(t[0])) * OSM_TILE_SIZE + int(p[0])
|
177
|
+
target_y_min = (y - int(t[1])) * OSM_TILE_SIZE + int(p[1])
|
178
|
+
target_x_max = target_x_min + OSM_TILE_SIZE
|
179
|
+
target_y_max = target_y_min + OSM_TILE_SIZE
|
180
|
+
|
181
|
+
if target_x_min < 0:
|
182
|
+
source_x_min -= target_x_min
|
183
|
+
target_x_min = 0
|
184
|
+
if target_y_min < 0:
|
185
|
+
source_y_min -= target_y_min
|
186
|
+
target_y_min = 0
|
187
|
+
if target_x_max > target[0]:
|
188
|
+
a = target_x_max - target[0]
|
189
|
+
target_x_max -= a
|
190
|
+
source_x_max -= a
|
191
|
+
if target_y_max > target[1]:
|
192
|
+
a = target_y_max - target[1]
|
193
|
+
target_y_max -= a
|
194
|
+
source_y_max -= a
|
195
|
+
|
196
|
+
if source_x_max < 0 or source_y_max < 0:
|
197
|
+
continue
|
198
|
+
|
199
|
+
tile = np.array(get_tile(zoom, x, y)) / 255
|
200
|
+
|
201
|
+
background[target_y_min:target_y_max, target_x_min:target_x_max] = tile[
|
202
|
+
source_y_min:source_y_max, source_x_min:source_x_max, :3
|
203
|
+
]
|
204
|
+
|
205
|
+
return background
|
206
|
+
|
207
|
+
|
145
208
|
def convert_to_grayscale(image: np.ndarray) -> np.ndarray:
|
146
209
|
image = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2)
|
147
210
|
image = np.dstack((image, image, image))
|
@@ -53,6 +53,8 @@ _strava_last_activity_date_path = _cache_dir / "strava-last-activity-date.json"
|
|
53
53
|
|
54
54
|
_new_config_file = pathlib.Path("config.json")
|
55
55
|
|
56
|
+
_activity_meta_override_dir = pathlib.Path("Metadata Override")
|
57
|
+
|
56
58
|
|
57
59
|
cache_dir = dir_wrapper(_cache_dir)
|
58
60
|
|
@@ -63,6 +65,7 @@ activity_enriched_meta_dir = dir_wrapper(_activity_enriched_meta_dir)
|
|
63
65
|
activity_enriched_time_series_dir = dir_wrapper(_activity_enriched_time_series_dir)
|
64
66
|
tiles_per_time_series = dir_wrapper(_tiles_per_time_series)
|
65
67
|
strava_api_dir = dir_wrapper(_strava_api_dir)
|
68
|
+
activity_meta_override_dir = dir_wrapper(_activity_meta_override_dir)
|
66
69
|
|
67
70
|
activities_file = file_wrapper(_activities_file)
|
68
71
|
strava_dynamic_config_path = file_wrapper(_strava_dynamic_config_path)
|
@@ -10,6 +10,7 @@ from typing import Optional
|
|
10
10
|
from tqdm import tqdm
|
11
11
|
|
12
12
|
from geo_activity_playground.core.activities import ActivityMeta
|
13
|
+
from geo_activity_playground.core.config import Config
|
13
14
|
from geo_activity_playground.core.paths import activity_extracted_dir
|
14
15
|
from geo_activity_playground.core.paths import activity_extracted_meta_dir
|
15
16
|
from geo_activity_playground.core.paths import activity_extracted_time_series_dir
|
@@ -24,13 +25,16 @@ ACTIVITY_DIR = pathlib.Path("Activities")
|
|
24
25
|
|
25
26
|
|
26
27
|
def import_from_directory(
|
27
|
-
metadata_extraction_regexes: list[str], num_processes: Optional[int]
|
28
|
+
metadata_extraction_regexes: list[str], num_processes: Optional[int], config: Config
|
28
29
|
) -> None:
|
29
30
|
|
30
31
|
activity_paths = [
|
31
32
|
path
|
32
33
|
for path in ACTIVITY_DIR.rglob("*.*")
|
33
|
-
if path.is_file()
|
34
|
+
if path.is_file()
|
35
|
+
and path.suffixes
|
36
|
+
and not path.stem.startswith(".")
|
37
|
+
and not path.suffix in config.ignore_suffixes
|
34
38
|
]
|
35
39
|
work_tracker = WorkTracker(activity_extracted_dir() / "work-tracker-extract.pickle")
|
36
40
|
new_activity_paths = work_tracker.filter(activity_paths)
|
@@ -1,21 +1,29 @@
|
|
1
|
+
import json
|
1
2
|
import urllib.parse
|
2
3
|
from collections.abc import Collection
|
3
4
|
|
4
5
|
from flask import Blueprint
|
6
|
+
from flask import redirect
|
5
7
|
from flask import render_template
|
8
|
+
from flask import request
|
6
9
|
from flask import Response
|
10
|
+
from flask import url_for
|
7
11
|
|
8
12
|
from ...core.activities import ActivityRepository
|
9
13
|
from ...explorer.tile_visits import TileVisitAccessor
|
10
14
|
from .controller import ActivityController
|
11
15
|
from geo_activity_playground.core.config import Config
|
16
|
+
from geo_activity_playground.core.paths import activity_meta_override_dir
|
12
17
|
from geo_activity_playground.core.privacy_zones import PrivacyZone
|
18
|
+
from geo_activity_playground.webui.authenticator import Authenticator
|
19
|
+
from geo_activity_playground.webui.authenticator import needs_authentication
|
13
20
|
|
14
21
|
|
15
22
|
def make_activity_blueprint(
|
16
23
|
repository: ActivityRepository,
|
17
24
|
tile_visit_accessor: TileVisitAccessor,
|
18
25
|
config: Config,
|
26
|
+
authenticator: Authenticator,
|
19
27
|
) -> Blueprint:
|
20
28
|
blueprint = Blueprint("activity", __name__, template_folder="templates")
|
21
29
|
|
@@ -44,14 +52,58 @@ def make_activity_blueprint(
|
|
44
52
|
def day(year: str, month: str, day: str):
|
45
53
|
return render_template(
|
46
54
|
"activity/day.html.j2",
|
47
|
-
**activity_controller.render_day(int(year), int(month), int(day))
|
55
|
+
**activity_controller.render_day(int(year), int(month), int(day)),
|
48
56
|
)
|
49
57
|
|
50
58
|
@blueprint.route("/name/<name>")
|
51
59
|
def name(name: str):
|
52
60
|
return render_template(
|
53
61
|
"activity/name.html.j2",
|
54
|
-
**activity_controller.render_name(urllib.parse.unquote(name))
|
62
|
+
**activity_controller.render_name(urllib.parse.unquote(name)),
|
63
|
+
)
|
64
|
+
|
65
|
+
@blueprint.route("/edit/<id>", methods=["GET", "POST"])
|
66
|
+
@needs_authentication(authenticator)
|
67
|
+
def edit(id: str):
|
68
|
+
activity_id = int(id)
|
69
|
+
activity = repository.get_activity_by_id(activity_id)
|
70
|
+
override_file = activity_meta_override_dir() / f"{activity_id}.json"
|
71
|
+
if override_file.exists():
|
72
|
+
with open(override_file) as f:
|
73
|
+
override = json.load(f)
|
74
|
+
else:
|
75
|
+
override = {}
|
76
|
+
|
77
|
+
if request.method == "POST":
|
78
|
+
override = {}
|
79
|
+
if value := request.form.get("name"):
|
80
|
+
override["name"] = value
|
81
|
+
repository.meta.loc[activity_id, "name"] = value
|
82
|
+
if value := request.form.get("kind"):
|
83
|
+
override["kind"] = value
|
84
|
+
repository.meta.loc[activity_id, "kind"] = value
|
85
|
+
if value := request.form.get("equipment"):
|
86
|
+
override["equipment"] = value
|
87
|
+
repository.meta.loc[activity_id, "equipment"] = value
|
88
|
+
if value := request.form.get("commute"):
|
89
|
+
override["commute"] = True
|
90
|
+
repository.meta.loc[activity_id, "commute"] = True
|
91
|
+
if value := request.form.get("consider_for_achievements"):
|
92
|
+
override["consider_for_achievements"] = True
|
93
|
+
repository.meta.loc[activity_id, "consider_for_achievements"] = True
|
94
|
+
|
95
|
+
with open(override_file, "w") as f:
|
96
|
+
json.dump(override, f, ensure_ascii=False, indent=4, sort_keys=True)
|
97
|
+
|
98
|
+
repository.save()
|
99
|
+
|
100
|
+
return redirect(url_for(".show", id=activity_id))
|
101
|
+
|
102
|
+
return render_template(
|
103
|
+
"activity/edit.html.j2",
|
104
|
+
activity_id=activity_id,
|
105
|
+
activity=activity,
|
106
|
+
override=override,
|
55
107
|
)
|
56
108
|
|
57
109
|
return blueprint
|
@@ -23,9 +23,11 @@ from geo_activity_playground.core.config import Config
|
|
23
23
|
from geo_activity_playground.core.heart_rate import HeartRateZoneComputer
|
24
24
|
from geo_activity_playground.core.heatmap import add_margin_to_geo_bounds
|
25
25
|
from geo_activity_playground.core.heatmap import build_map_from_tiles
|
26
|
+
from geo_activity_playground.core.heatmap import build_map_from_tiles_around_center
|
26
27
|
from geo_activity_playground.core.heatmap import GeoBounds
|
27
28
|
from geo_activity_playground.core.heatmap import get_bounds
|
28
29
|
from geo_activity_playground.core.heatmap import get_sensible_zoom_level
|
30
|
+
from geo_activity_playground.core.heatmap import OSM_MAX_ZOOM
|
29
31
|
from geo_activity_playground.core.heatmap import OSM_TILE_SIZE
|
30
32
|
from geo_activity_playground.core.heatmap import PixelBounds
|
31
33
|
from geo_activity_playground.core.heatmap import TileBounds
|
@@ -459,42 +461,55 @@ def make_sharepic(
|
|
459
461
|
time_series: pd.DataFrame,
|
460
462
|
sharepic_suppressed_fields: list[str],
|
461
463
|
) -> bytes:
|
462
|
-
|
464
|
+
tile_x = time_series["x"]
|
465
|
+
tile_y = time_series["y"]
|
466
|
+
tile_width = tile_x.max() - tile_x.min()
|
467
|
+
tile_height = tile_y.max() - tile_y.min()
|
468
|
+
|
469
|
+
target_width = 600
|
470
|
+
target_height = 600
|
471
|
+
footer_height = 100
|
472
|
+
target_map_height = target_height - footer_height
|
473
|
+
|
474
|
+
zoom = int(
|
475
|
+
min(
|
476
|
+
np.log2(target_width / tile_width / OSM_TILE_SIZE),
|
477
|
+
np.log2(target_map_height / tile_height / OSM_TILE_SIZE),
|
478
|
+
OSM_MAX_ZOOM,
|
479
|
+
)
|
480
|
+
)
|
463
481
|
|
464
|
-
|
465
|
-
|
466
|
-
tile_bounds = get_sensible_zoom_level(geo_bounds, (1500, 1500))
|
467
|
-
tile_bounds = make_tile_bounds_square(tile_bounds)
|
468
|
-
background = build_map_from_tiles(tile_bounds)
|
469
|
-
# background = convert_to_grayscale(background)
|
482
|
+
tile_xz = tile_x * 2**zoom
|
483
|
+
tile_yz = tile_y * 2**zoom
|
470
484
|
|
471
|
-
|
472
|
-
|
485
|
+
tile_xz_center = (
|
486
|
+
(tile_xz.max() + tile_xz.min()) / 2,
|
487
|
+
(tile_yz.max() + tile_yz.min()) / 2,
|
488
|
+
)
|
473
489
|
|
474
|
-
background =
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
490
|
+
background = build_map_from_tiles_around_center(
|
491
|
+
tile_xz_center,
|
492
|
+
zoom,
|
493
|
+
(target_width, target_height),
|
494
|
+
(target_width, target_map_height),
|
495
|
+
)
|
479
496
|
|
480
497
|
img = Image.fromarray((background * 255).astype("uint8"), "RGB")
|
481
498
|
draw = ImageDraw.Draw(img, mode="RGBA")
|
482
499
|
|
483
500
|
for _, group in time_series.groupby("segment_id"):
|
484
|
-
xs, ys = compute_tile_float(
|
485
|
-
group["latitude"], group["longitude"], tile_bounds.zoom
|
486
|
-
)
|
487
501
|
yx = list(
|
488
|
-
(
|
489
|
-
|
490
|
-
|
502
|
+
zip(
|
503
|
+
(tile_xz - tile_xz_center[0]) * OSM_TILE_SIZE + target_width / 2,
|
504
|
+
(tile_yz - tile_xz_center[1]) * OSM_TILE_SIZE + target_map_height / 2,
|
491
505
|
)
|
492
|
-
for x, y in zip(xs, ys)
|
493
506
|
)
|
494
507
|
|
495
508
|
draw.line(yx, fill="red", width=4)
|
496
509
|
|
497
|
-
draw.rectangle(
|
510
|
+
draw.rectangle(
|
511
|
+
[0, img.height - footer_height, img.width, img.height], fill=(0, 0, 0, 180)
|
512
|
+
)
|
498
513
|
|
499
514
|
facts = {
|
500
515
|
"kind": f"{activity['kind']}",
|
@@ -515,19 +530,20 @@ def make_sharepic(
|
|
515
530
|
if not key in sharepic_suppressed_fields
|
516
531
|
}
|
517
532
|
|
518
|
-
draw.text(
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
533
|
+
draw.text(
|
534
|
+
(35, img.height - footer_height + 10),
|
535
|
+
" ".join(facts.values()),
|
536
|
+
font_size=20,
|
537
|
+
)
|
523
538
|
|
524
|
-
|
525
|
-
|
526
|
-
|
539
|
+
draw.text(
|
540
|
+
(img.width - 250, img.height - 20),
|
541
|
+
"Map: © Open Street Map Contributors",
|
542
|
+
font_size=14,
|
543
|
+
)
|
527
544
|
|
528
545
|
f = io.BytesIO()
|
529
546
|
img.save(f, format="png")
|
530
|
-
# pl.imsave(f, background, format="png")
|
531
547
|
return bytes(f.getbuffer())
|
532
548
|
|
533
549
|
|
@@ -58,9 +58,9 @@
|
|
58
58
|
<td><span style="color: {{ activity['color'] }};">█</span> <a
|
59
59
|
href="{{ url_for('activity.show', id=activity.id) }}">{{
|
60
60
|
activity.name }}</a></td>
|
61
|
-
<td>{{ activity.start }}</td>
|
61
|
+
<td>{{ activity.start|dt }}</td>
|
62
62
|
<td>{{ activity.distance_km | round(1) }}</td>
|
63
|
-
<td>{{ activity.elapsed_time }}</td>
|
63
|
+
<td>{{ activity.elapsed_time|td }}</td>
|
64
64
|
<td>{{ activity["equipment"] }}</td>
|
65
65
|
<td>{{ activity["kind"] }}</td>
|
66
66
|
</tr>
|
@@ -70,7 +70,7 @@
|
|
70
70
|
<td><b>Total</b></td>
|
71
71
|
<td></td>
|
72
72
|
<td><b>{{ total_distance | round(1) }}</b></td>
|
73
|
-
<td><b>{{ total_elapsed_time }}</b></td>
|
73
|
+
<td><b>{{ total_elapsed_time|td }}</b></td>
|
74
74
|
<td></td>
|
75
75
|
<td></td>
|
76
76
|
</tr>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<h1 class="mb-3">Edit Activity</h1>
|
6
|
+
|
7
|
+
<form method="POST">
|
8
|
+
<div class="mb-3">
|
9
|
+
<label for="name" class="form-label">Name</label>
|
10
|
+
<input type="text" class="form-control" id="name" name="name" value="{{ override['name'] }}" />
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<div class="mb-3">
|
14
|
+
<label for="kind" class="form-label">Kind</label>
|
15
|
+
<input type="text" class="form-control" id="kind" name="kind" value="{{ override['kind'] }}" />
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<div class="mb-3">
|
19
|
+
<label for="equipment" class="form-label">Equipment</label>
|
20
|
+
<input type="text" class="form-control" id="equipment" name="equipment" value="{{ override['equipment'] }}" />
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div class="mb-3">
|
24
|
+
<div class="form-check">
|
25
|
+
<input type="checkbox" class="form-check-input" id="commute" name="commute" {% if override['commute'] %}
|
26
|
+
checked {% endif %} />
|
27
|
+
<label for="commute" class="form-check-label">Commute</label>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<div class="mb-3">
|
32
|
+
<div class="form-check">
|
33
|
+
<input type="checkbox" class="form-check-input" id="consider_for_achievements"
|
34
|
+
name="consider_for_achievements" {% if override['consider_for_achievements'] %} checked {% endif %} />
|
35
|
+
<label for="consider_for_achievements" class="form-check-label">Consider for achievements</label>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
40
|
+
</form>
|
41
|
+
|
42
|
+
{% endblock %}
|
@@ -66,9 +66,9 @@
|
|
66
66
|
<tr>
|
67
67
|
<td><span style="color: {{ activity['color'] }};">█</span> <a href="{{ url_for('activity.show', id=activity.id) }}">{{
|
68
68
|
activity.name }}</a></td>
|
69
|
-
<td>{{ activity.start }}</td>
|
69
|
+
<td>{{ activity.start|dt }}</td>
|
70
70
|
<td>{{ activity.distance_km | round(1) }}</td>
|
71
|
-
<td>{{ activity.elapsed_time }}</td>
|
71
|
+
<td>{{ activity.elapsed_time|td }}</td>
|
72
72
|
<td>{{ activity["equipment"] }}</td>
|
73
73
|
<td>{{ activity["kind"] }}</td>
|
74
74
|
</tr>
|
@@ -19,9 +19,9 @@
|
|
19
19
|
<dt>Distance</dt>
|
20
20
|
<dd>{{ activity.distance_km|round(1) }} km</dd>
|
21
21
|
<dt>Elapsed time</dt>
|
22
|
-
<dd>{{ activity.elapsed_time }}</dd>
|
22
|
+
<dd>{{ activity.elapsed_time|td }}</dd>
|
23
23
|
<dt>Moving time</dt>
|
24
|
-
<dd>{{ activity.moving_time }}</dd>
|
24
|
+
<dd>{{ activity.moving_time|td }}</dd>
|
25
25
|
<dt>Start time</dt>
|
26
26
|
<dd><a href="{{ url_for('activity.day', year=date.year, month=date.month, day=date.day) }}">{{ date }}</a>
|
27
27
|
{{ time }}
|
@@ -31,7 +31,7 @@
|
|
31
31
|
<dt>Steps</dt>
|
32
32
|
<dd>{{ activity.steps }}</dd>
|
33
33
|
<dt>Equipment</dt>
|
34
|
-
<dd>{{ activity
|
34
|
+
<dd>{{ activity['equipment'] }}</dd>
|
35
35
|
<dt>New Explorer Tiles</dt>
|
36
36
|
<dd>{{ new_tiles[14] }}</dd>
|
37
37
|
<dt>New Squadratinhos</dt>
|
@@ -41,6 +41,8 @@
|
|
41
41
|
<dt>Source path</dt>
|
42
42
|
<dd>{{ activity.path }}</dd>
|
43
43
|
</dl>
|
44
|
+
|
45
|
+
<a href="{{ url_for('.edit', id=activity['id']) }}" class="btn btn-secondary btn-small">Edit metadata</a>
|
44
46
|
</div>
|
45
47
|
<div class="col-8">
|
46
48
|
<div id="activity-map" style="height: 500px;" class="mb-3"></div>
|
@@ -201,10 +203,10 @@
|
|
201
203
|
<tbody>
|
202
204
|
{% for other_activity in similar_activites %}
|
203
205
|
<tr>
|
204
|
-
<td><a href="{{ url_for('.show', id=other_activity.id) }}">{{ other_activity.start
|
206
|
+
<td><a href="{{ url_for('.show', id=other_activity.id) }}">{{ other_activity.start|dt
|
205
207
|
}}</a></td>
|
206
208
|
<td>{{ other_activity.distance_km | round(1) }}</td>
|
207
|
-
<td>{{ other_activity.elapsed_time }}</td>
|
209
|
+
<td>{{ other_activity.elapsed_time|td }}</td>
|
208
210
|
<td>{{ other_activity["equipment"] }}</td>
|
209
211
|
<td>{{ other_activity["kind"] }}</td>
|
210
212
|
</tr>
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import datetime
|
1
2
|
import importlib
|
2
3
|
import json
|
3
4
|
import pathlib
|
@@ -60,6 +61,18 @@ def web_ui_main(
|
|
60
61
|
app.config["UPLOAD_FOLDER"] = "Activities"
|
61
62
|
app.secret_key = get_secret_key()
|
62
63
|
|
64
|
+
@app.template_filter()
|
65
|
+
def dt(value: datetime.datetime):
|
66
|
+
return value.strftime("%Y-%m-%d %H:%M")
|
67
|
+
|
68
|
+
@app.template_filter()
|
69
|
+
def td(v: datetime.timedelta):
|
70
|
+
seconds = v.total_seconds()
|
71
|
+
h = int(seconds // 3600)
|
72
|
+
m = int(seconds // 60 % 60)
|
73
|
+
s = int(seconds // 1 % 60)
|
74
|
+
return f"{h}:{m:02d}:{s:02d}"
|
75
|
+
|
63
76
|
authenticator = Authenticator(config_accessor())
|
64
77
|
|
65
78
|
route_start(app, repository, config_accessor())
|
@@ -68,9 +81,7 @@ def web_ui_main(
|
|
68
81
|
|
69
82
|
app.register_blueprint(
|
70
83
|
make_activity_blueprint(
|
71
|
-
repository,
|
72
|
-
tile_visit_accessor,
|
73
|
-
config_accessor(),
|
84
|
+
repository, tile_visit_accessor, config_accessor(), authenticator
|
74
85
|
),
|
75
86
|
url_prefix="/activity",
|
76
87
|
)
|
@@ -90,23 +90,25 @@ class HeatmapController:
|
|
90
90
|
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
91
91
|
tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
92
92
|
activity_ids = self.activities_per_tile[z].get((x, y), set())
|
93
|
-
|
93
|
+
activity_ids_kind = set()
|
94
|
+
for activity_id in activity_ids:
|
95
|
+
activity = self._repository.get_activity_by_id(activity_id)
|
96
|
+
if activity["kind"] == kind:
|
97
|
+
activity_ids_kind.add(activity_id)
|
98
|
+
if activity_ids_kind:
|
94
99
|
with work_tracker(
|
95
100
|
tile_count_cache_path.with_suffix(".json")
|
96
101
|
) as parsed_activities:
|
97
|
-
if parsed_activities -
|
102
|
+
if parsed_activities - activity_ids_kind:
|
98
103
|
logger.warning(
|
99
104
|
f"Resetting heatmap cache for {kind=}/{x=}/{y=}/{z=} because activities have been removed."
|
100
105
|
)
|
101
106
|
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
102
107
|
parsed_activities.clear()
|
103
|
-
for activity_id in
|
108
|
+
for activity_id in activity_ids_kind:
|
104
109
|
if activity_id in parsed_activities:
|
105
110
|
continue
|
106
111
|
parsed_activities.add(activity_id)
|
107
|
-
activity = self._repository.get_activity_by_id(activity_id)
|
108
|
-
if activity["kind"] != kind:
|
109
|
-
continue
|
110
112
|
time_series = self._repository.get_time_series(activity_id)
|
111
113
|
for _, group in time_series.groupby("segment_id"):
|
112
114
|
xy_pixels = (
|
@@ -78,10 +78,10 @@
|
|
78
78
|
{% for index, activity in activities %}
|
79
79
|
<tr>
|
80
80
|
<td><a href="{{ url_for('activity.show', id=activity['id']) }}">{{ activity['name'] }}</a></td>
|
81
|
-
<td>{{ activity['start'] }}</td>
|
81
|
+
<td>{{ activity['start']|dt }}</td>
|
82
82
|
<td>{{ activity['kind'] }}</td>
|
83
83
|
<td>{{ '%.1f' % activity["distance_km"] }} km</td>
|
84
|
-
<td>{{ activity.elapsed_time }}</td>
|
84
|
+
<td>{{ activity.elapsed_time|td }}</td>
|
85
85
|
</tr>
|
86
86
|
{% endfor %}
|
87
87
|
</tbody>
|
@@ -21,7 +21,7 @@ class SummaryController:
|
|
21
21
|
def render(self) -> dict:
|
22
22
|
kind_scale = make_kind_scale(self._repository.meta, self._config)
|
23
23
|
df = embellished_activities(self._repository.meta)
|
24
|
-
df = df.loc[df["consider_for_achievements"]]
|
24
|
+
# df = df.loc[df["consider_for_achievements"]]
|
25
25
|
|
26
26
|
year_kind_total = (
|
27
27
|
df[["year", "kind", "distance_km", "hours"]]
|
@@ -123,7 +123,7 @@
|
|
123
123
|
</p>
|
124
124
|
<p class="card-text"><small class="text-body-secondary"></small>{{ activity.kind }} with {{
|
125
125
|
(activity.distance_km)|round(1) }} km in {{
|
126
|
-
activity.elapsed_time }} on {{ activity.start }}</small></p>
|
126
|
+
activity.elapsed_time|td }} on {{ activity.start|dt }}</small></p>
|
127
127
|
</div>
|
128
128
|
</div>
|
129
129
|
</div>
|
@@ -49,8 +49,8 @@
|
|
49
49
|
<h5 class="card-title">{{ elem.activity["name"] }}</h5>
|
50
50
|
</a>
|
51
51
|
<p class="card-text">{{ elem.activity.kind }} with {{ (elem.activity.distance_km)|round(1) }} km in {{
|
52
|
-
elem.activity.elapsed_time }}</p>
|
53
|
-
<p class="card-text"><small class="text-body-secondary">{{ elem.activity.start }}</small></p>
|
52
|
+
elem.activity.elapsed_time|td }}</p>
|
53
|
+
<p class="card-text"><small class="text-body-secondary">{{ elem.activity.start|dt }}</small></p>
|
54
54
|
</div>
|
55
55
|
</div>
|
56
56
|
</div>
|
@@ -102,8 +102,7 @@ def scan_for_activities(
|
|
102
102
|
) -> None:
|
103
103
|
if pathlib.Path("Activities").exists():
|
104
104
|
import_from_directory(
|
105
|
-
config.metadata_extraction_regexes,
|
106
|
-
config.num_processes,
|
105
|
+
config.metadata_extraction_regexes, config.num_processes, config
|
107
106
|
)
|
108
107
|
if pathlib.Path("Strava Export").exists():
|
109
108
|
import_from_strava_checkout()
|
{geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/RECORD
RENAMED
@@ -1,13 +1,13 @@
|
|
1
1
|
geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
geo_activity_playground/__main__.py,sha256=MBZn9K1m3PofiPNTtpsSTVCyB_Gz95TjVP-nb9v_-JE,3989
|
3
3
|
geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
geo_activity_playground/core/activities.py,sha256=
|
5
|
-
geo_activity_playground/core/config.py,sha256=
|
4
|
+
geo_activity_playground/core/activities.py,sha256=soxMtdijnkZ1bYZU0q6wuM8NPNFoUpLwYp3IvBOaKJY,6927
|
5
|
+
geo_activity_playground/core/config.py,sha256=T6u8Ha2yUTmSA-TcI0_yg_vNIh-JdAyKt7vs-cfQmuE,4817
|
6
6
|
geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
|
7
7
|
geo_activity_playground/core/enrichment.py,sha256=fUmk6avy_rqePlHmJQFTQhAxjgIRaxxmq18N2OSXBBg,7771
|
8
8
|
geo_activity_playground/core/heart_rate.py,sha256=IwMt58TpjOYqpAxtsj07zP2ttpN_J3GZeiv-qGhYyJc,1598
|
9
|
-
geo_activity_playground/core/heatmap.py,sha256=
|
10
|
-
geo_activity_playground/core/paths.py,sha256=
|
9
|
+
geo_activity_playground/core/heatmap.py,sha256=KqqXo9ayppwXU3VEkhqtcyC-EPkRUVZ-J0EK6FhR50M,6474
|
10
|
+
geo_activity_playground/core/paths.py,sha256=RBeUi38riP_msTGPy1TsPRNiblzE-lFivaJSLULE8b0,2503
|
11
11
|
geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
|
12
12
|
geo_activity_playground/core/similarity.py,sha256=Jo8jRViuORCxdIGvyaflgsQhwu9S_jn10a450FRL18A,3159
|
13
13
|
geo_activity_playground/core/tasks.py,sha256=aMDBWJqp6ek2ao6G6Xa8GOSZbcQqXoWL74SGRowRPIk,2942
|
@@ -22,7 +22,7 @@ geo_activity_playground/explorer/video.py,sha256=ROAmV9shfJyqTgnXVD41KFORiwnRgVp
|
|
22
22
|
geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
23
|
geo_activity_playground/importers/activity_parsers.py,sha256=XNQs0ziqAcVqIoiLAX5Ndmhb6v__29XdjUPvNvc7oBI,11082
|
24
24
|
geo_activity_playground/importers/csv_parser.py,sha256=O1pP5GLhWhnWcy2Lsrr9g17Zspuibpt-GtZ3ZS5eZF4,2143
|
25
|
-
geo_activity_playground/importers/directory.py,sha256=
|
25
|
+
geo_activity_playground/importers/directory.py,sha256=CA-vFOMm8G4MSM_Q09OwQKduCApL2PWaxLTVxgw_xpw,5908
|
26
26
|
geo_activity_playground/importers/strava_api.py,sha256=cJCZsLemhOlxTtZh0z_npidgae9SD5HyEUry2uvem_A,7775
|
27
27
|
geo_activity_playground/importers/strava_checkout.py,sha256=N-uGTkhBJMC7cPYjRRXHOSLwpK3wc6aaSrY2RQfSitA,9419
|
28
28
|
geo_activity_playground/importers/test_csv_parser.py,sha256=LXqva7GuSAfXYE2zZQrg-69lCtfy5MxLSq6BRwL_VyI,1191
|
@@ -30,13 +30,14 @@ geo_activity_playground/importers/test_directory.py,sha256=ljXokx7q0OgtHvEdHftcQ
|
|
30
30
|
geo_activity_playground/importers/test_strava_api.py,sha256=4vX7wDr1a9aRh8myxNrIq6RwDBbP8ZeoXXPc10CAbW4,431
|
31
31
|
geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
32
|
geo_activity_playground/webui/activity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
33
|
-
geo_activity_playground/webui/activity/blueprint.py,sha256=
|
34
|
-
geo_activity_playground/webui/activity/controller.py,sha256=
|
35
|
-
geo_activity_playground/webui/activity/templates/activity/day.html.j2,sha256=
|
33
|
+
geo_activity_playground/webui/activity/blueprint.py,sha256=Ub2mC9S9TII7CJaaWahnbNtT76uOGKNDWE0-j2C56CA,3893
|
34
|
+
geo_activity_playground/webui/activity/controller.py,sha256=zOPoj4N-B-x1O_qp5ZUvlT5gmDjpIGuqKqnv4f1-B54,19570
|
35
|
+
geo_activity_playground/webui/activity/templates/activity/day.html.j2,sha256=o18e-TMtgCrY7iroInVhRA267l-o6uGNlstIwsvFnww,2735
|
36
|
+
geo_activity_playground/webui/activity/templates/activity/edit.html.j2,sha256=ckcTTxcQOhmvvAGNTEOtWCUG4LhvO4HfQImlIa5qKs8,1530
|
36
37
|
geo_activity_playground/webui/activity/templates/activity/lines.html.j2,sha256=5gB1aDjRgi_RventenRfC10_FtMT4ch_VuWvA9AMlBY,1121
|
37
|
-
geo_activity_playground/webui/activity/templates/activity/name.html.j2,sha256=
|
38
|
-
geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=
|
39
|
-
geo_activity_playground/webui/app.py,sha256=
|
38
|
+
geo_activity_playground/webui/activity/templates/activity/name.html.j2,sha256=npciXBo7_94_tCE0X7RLFHws8mgHzs4VgbOnyA9rsOI,2451
|
39
|
+
geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=MndgjmSi6T2xbIietSI-5n1hi7E1tD4eCoQP0tD3Ing,6989
|
40
|
+
geo_activity_playground/webui/app.py,sha256=zDpLVGa0-_uoy9KQDcY8Z52RENhziDOfxfWv744GQAw,4616
|
40
41
|
geo_activity_playground/webui/auth/blueprint.py,sha256=Lx-ZvMnfHLC1CMre1xPQI3k_pCtQoZvgRhtmafULzoE,812
|
41
42
|
geo_activity_playground/webui/auth/templates/auth/index.html.j2,sha256=ILQ5HvTEYc3OrtOAIFt1VrqWorVD70V9DC342znmP70,579
|
42
43
|
geo_activity_playground/webui/authenticator.py,sha256=k278OEVuOfAmTGT4F2X4pqSTwwkK_FA87EIhAeysEqc,1416
|
@@ -60,11 +61,11 @@ geo_activity_playground/webui/explorer/controller.py,sha256=pIzWh0TpLJgKQZlS325-
|
|
60
61
|
geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=u2htecx-XwINRiINHFN6EZDaRXVtiape1OCUZexTBU8,7049
|
61
62
|
geo_activity_playground/webui/heatmap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
62
63
|
geo_activity_playground/webui/heatmap/blueprint.py,sha256=bjQu-HL3QN5UpJ6tHOifhcLGlPr_hIKvaRu78md4JqM,1470
|
63
|
-
geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=
|
64
|
+
geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=LMBglDOhcLjvkq-7hr1B6IhW_TiBDZ1NIFP5dqhLCC4,7503
|
64
65
|
geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2,sha256=YLeu6P4djl8G4qAXR6DhetseqrbOodN7aN4coocknc4,1875
|
65
66
|
geo_activity_playground/webui/plot_util.py,sha256=pTTQoqOCkLVjkgOit7mbry28kMruZIL8amZozSzEpxQ,283
|
66
67
|
geo_activity_playground/webui/search/blueprint.py,sha256=7TDsiqEowMyHNlFImk-hCGso69KOieG4rfJnLRHpRz8,3300
|
67
|
-
geo_activity_playground/webui/search/templates/search/index.html.j2,sha256=
|
68
|
+
geo_activity_playground/webui/search/templates/search/index.html.j2,sha256=d39uhteoY6JOZePqhLIYLERqkckW3oghMnTUZa7X1J8,3798
|
68
69
|
geo_activity_playground/webui/settings/blueprint.py,sha256=Jeh2MwRmCNF6BfwrHSUixLKOTPFlwZ4Mrb68Botr6Q8,9487
|
69
70
|
geo_activity_playground/webui/settings/controller.py,sha256=MIZVBfoGNvmJnB_ECV_x5eH2i6gDZvkWSQ4PcSKyLKs,9170
|
70
71
|
geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2,sha256=VYwddpObD1RpeTH5Dm4y7VtmT7kwURDCIjxyzJeq08c,495
|
@@ -99,20 +100,20 @@ geo_activity_playground/webui/static/web-app-manifest-192x192.png,sha256=eEImN6i
|
|
99
100
|
geo_activity_playground/webui/static/web-app-manifest-512x512.png,sha256=vU9oQ4HnQerFDZVzcAT9twj4_Doc6_9v9wVvoRI-f_E,48318
|
100
101
|
geo_activity_playground/webui/summary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
101
102
|
geo_activity_playground/webui/summary/blueprint.py,sha256=tfA2aPF19yKwkQOb5lPQBySoQYYhTn49Iuh0SYvsGP8,593
|
102
|
-
geo_activity_playground/webui/summary/controller.py,sha256=
|
103
|
-
geo_activity_playground/webui/summary/templates/summary/index.html.j2,sha256=
|
104
|
-
geo_activity_playground/webui/templates/home.html.j2,sha256=
|
103
|
+
geo_activity_playground/webui/summary/controller.py,sha256=UU6ClARzdvUQnIaSyuG3mbNxtuCjYCUM22P1aRFwOuQ,9445
|
104
|
+
geo_activity_playground/webui/summary/templates/summary/index.html.j2,sha256=S_kpXPldrxIAEBdlG0YlXlvMHI4dQc4QZtejhHM4_N8,4472
|
105
|
+
geo_activity_playground/webui/templates/home.html.j2,sha256=EvEgvr_JeppGqLEJzcDc0kL-8e4OUV8aleWTP5eDeh8,2173
|
105
106
|
geo_activity_playground/webui/templates/page.html.j2,sha256=znTbtD2NALrhmUN_Q-F1ElGlKtecoUv8vOCcUtojrdI,11134
|
106
107
|
geo_activity_playground/webui/tile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
107
108
|
geo_activity_playground/webui/tile/blueprint.py,sha256=cK0o2Z3BrLycgF9zw0F8s9qF-JaYDbF5Gog-GXDtUZ8,943
|
108
109
|
geo_activity_playground/webui/tile/controller.py,sha256=PISh4vKs27b-LxFfTARtr5RAwHFresA1Kw1MDcERSRU,1221
|
109
110
|
geo_activity_playground/webui/upload/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
110
111
|
geo_activity_playground/webui/upload/blueprint.py,sha256=xX9scEmleN_dL03jfhWRh5yI1WsFyhxUFiS_Ul2HWy4,1428
|
111
|
-
geo_activity_playground/webui/upload/controller.py,sha256=
|
112
|
+
geo_activity_playground/webui/upload/controller.py,sha256=EvoUnmKBo3QS2TLak7-yVZ16sEDyEB6Nf2MN_scHuhQ,4080
|
112
113
|
geo_activity_playground/webui/upload/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
|
113
114
|
geo_activity_playground/webui/upload/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
|
114
|
-
geo_activity_playground-0.
|
115
|
-
geo_activity_playground-0.
|
116
|
-
geo_activity_playground-0.
|
117
|
-
geo_activity_playground-0.
|
118
|
-
geo_activity_playground-0.
|
115
|
+
geo_activity_playground-0.32.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
|
116
|
+
geo_activity_playground-0.32.0.dist-info/METADATA,sha256=02jGl-aJPw8GRimnYlMTNECzPhE0ei6fpiidYbK7UDM,1612
|
117
|
+
geo_activity_playground-0.32.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
118
|
+
geo_activity_playground-0.32.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
|
119
|
+
geo_activity_playground-0.32.0.dist-info/RECORD,,
|
{geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/LICENSE
RENAMED
File without changes
|
File without changes
|