geo-activity-playground 0.26.3__py3-none-any.whl → 0.27.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/__main__.py +23 -20
- geo_activity_playground/core/activities.py +1 -44
- geo_activity_playground/core/config.py +111 -0
- geo_activity_playground/core/enrichment.py +11 -2
- geo_activity_playground/core/heart_rate.py +49 -0
- geo_activity_playground/core/paths.py +6 -0
- geo_activity_playground/core/tasks.py +14 -0
- geo_activity_playground/core/tiles.py +1 -1
- geo_activity_playground/explorer/tile_visits.py +23 -11
- geo_activity_playground/importers/csv_parser.py +73 -0
- geo_activity_playground/importers/directory.py +17 -8
- geo_activity_playground/importers/strava_api.py +20 -44
- geo_activity_playground/importers/strava_checkout.py +57 -32
- geo_activity_playground/importers/test_csv_parser.py +49 -0
- geo_activity_playground/webui/activity/blueprint.py +3 -4
- geo_activity_playground/webui/activity/controller.py +40 -14
- geo_activity_playground/webui/activity/templates/activity/show.html.j2 +6 -2
- geo_activity_playground/webui/app.py +26 -26
- geo_activity_playground/webui/eddington/controller.py +1 -1
- geo_activity_playground/webui/equipment/blueprint.py +5 -2
- geo_activity_playground/webui/equipment/controller.py +5 -6
- geo_activity_playground/webui/explorer/blueprint.py +14 -2
- geo_activity_playground/webui/explorer/controller.py +21 -1
- geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +12 -1
- geo_activity_playground/webui/settings/blueprint.py +106 -0
- geo_activity_playground/webui/settings/controller.py +228 -0
- geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +44 -0
- geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +102 -0
- geo_activity_playground/webui/settings/templates/settings/index.html.j2 +74 -0
- geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +30 -0
- geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +55 -0
- geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +81 -0
- geo_activity_playground/webui/{strava/templates/strava/client-id.html.j2 → settings/templates/settings/strava.html.j2} +17 -7
- geo_activity_playground/webui/templates/page.html.j2 +5 -1
- geo_activity_playground/webui/upload/blueprint.py +10 -1
- geo_activity_playground/webui/upload/controller.py +24 -11
- geo_activity_playground/webui/upload/templates/upload/reload.html.j2 +16 -0
- {geo_activity_playground-0.26.3.dist-info → geo_activity_playground-0.27.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.26.3.dist-info → geo_activity_playground-0.27.0.dist-info}/RECORD +42 -35
- geo_activity_playground/webui/strava/__init__.py +0 -0
- geo_activity_playground/webui/strava/blueprint.py +0 -33
- geo_activity_playground/webui/strava/controller.py +0 -49
- geo_activity_playground/webui/strava/templates/strava/connected.html.j2 +0 -14
- geo_activity_playground/webui/templates/settings.html.j2 +0 -24
- {geo_activity_playground-0.26.3.dist-info → geo_activity_playground-0.27.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.26.3.dist-info → geo_activity_playground-0.27.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.26.3.dist-info → geo_activity_playground-0.27.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
<div class="row">
|
5
|
+
<div class="col">
|
6
|
+
<h1>Settings</h1>
|
7
|
+
</div>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<p>Here are various settings that control how activities are gathered, analyzed and visualized.</p>
|
11
|
+
|
12
|
+
<div class="row mb-3">
|
13
|
+
<div class="row row-cols-1 row-cols-md-3 g-3">
|
14
|
+
<div class="col">
|
15
|
+
<div class="card">
|
16
|
+
<div class="card-body">
|
17
|
+
<h5 class="card-title">Equipment offsets</h5>
|
18
|
+
<p class="card-text">Not all activities with a given equipment are recorded? Just set an offset.</p>
|
19
|
+
<a href="{{ url_for('.equipment_offsets') }}" class="btn btn-primary">Set up equipment offsets</a>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
<div class="col">
|
24
|
+
<div class="card">
|
25
|
+
<div class="card-body">
|
26
|
+
<h5 class="card-title">Heart rate</h5>
|
27
|
+
<p class="card-text">Specify maximum heart rate to derive heart rate zones.</p>
|
28
|
+
<a href="{{ url_for('.heart_rate') }}" class="btn btn-primary">Set up heart rate</a>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
<div class="col">
|
33
|
+
<div class="card">
|
34
|
+
<div class="card-body">
|
35
|
+
<h5 class="card-title">Kinds without achievements</h5>
|
36
|
+
<p class="card-text">Recorded car rides that shouldn't count towards the explorer tiles? Exempt
|
37
|
+
activity kinds from consideration.</p>
|
38
|
+
<a href="{{ url_for('.kinds_without_achievements') }}" class="btn btn-primary">Set up kinds without
|
39
|
+
achievements</a>
|
40
|
+
</div>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
<div class="col">
|
44
|
+
<div class="card">
|
45
|
+
<div class="card-body">
|
46
|
+
<h5 class="card-title">Metadata extraction</h5>
|
47
|
+
<p class="card-text">When using activity files, one might want to extract metadata from the paths or
|
48
|
+
set defaults.</p>
|
49
|
+
<a href="{{ url_for('.metadata_extraction') }}" class="btn btn-primary">Set up metadata
|
50
|
+
extraction</a>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
<div class="col">
|
55
|
+
<div class="card">
|
56
|
+
<div class="card-body">
|
57
|
+
<h5 class="card-title">Privacy zones</h5>
|
58
|
+
<p class="card-text">Define geographic zones that shall be excluded from share pictures.</p>
|
59
|
+
<a href="{{ url_for('.privacy_zones') }}" class="btn btn-primary">Set up privacy zones</a>
|
60
|
+
</div>
|
61
|
+
</div>
|
62
|
+
</div>
|
63
|
+
<div class="col">
|
64
|
+
<div class="card">
|
65
|
+
<div class="card-body">
|
66
|
+
<h5 class="card-title">Strava API</h5>
|
67
|
+
<p class="card-text">Connect to the Strava API to download activities from there.</p>
|
68
|
+
<a href="{{ url_for('.strava') }}" class="btn btn-primary">Set up Strava API</a>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
{% endblock %}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<h1 class="mb-3">Kinds without Achievements</h1>
|
6
|
+
|
7
|
+
<p>Apart from bicycle rides and runs, you might also record your car and train rides. If you don't want to include these
|
8
|
+
for achievements like explorer tiles, you can enter the activity kinds that should not be considered here.</p>
|
9
|
+
|
10
|
+
<form method="POST">
|
11
|
+
<div class="row">
|
12
|
+
<div class="col-md-6">
|
13
|
+
{% for kind in kinds_without_achievements %}
|
14
|
+
<div class="mb-3">
|
15
|
+
<label for="kind_{{ loop.index }}" class="form-label">Kind</label>
|
16
|
+
<input type="text" class="form-control" id="kind_{{ loop.index }}" name="kind" value="{{ kind }}" />
|
17
|
+
</div>
|
18
|
+
{% endfor %}
|
19
|
+
<div class="mb-3">
|
20
|
+
<label for="regex_new" class="form-label">Kind</label>
|
21
|
+
<input type="text" class="form-control" id="kind_new" name="kind" />
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
|
26
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
27
|
+
</form>
|
28
|
+
|
29
|
+
|
30
|
+
{% endblock %}
|
@@ -0,0 +1,55 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<h1 class="mb-3">Metadata Extraction</h1>
|
6
|
+
|
7
|
+
<p>There are a few metadata fields that can be populated with information from the path of the activity file or by a
|
8
|
+
default value. These are:</p>
|
9
|
+
|
10
|
+
<ul>
|
11
|
+
<li><tt>kind</tt>: The kind of the activity, like "Ride" or "Run".</li>
|
12
|
+
<li><tt>equipment</tt>: Name for the equipment used, like "Red Roadbike".</li>
|
13
|
+
<li><tt>name</tt>: Name for the activity, like "Ride with Friends".</li>
|
14
|
+
</ul>
|
15
|
+
|
16
|
+
<p>By default these fields are populated with information from within the activity files. In case one hasn't set these
|
17
|
+
values correctly, it can be nice to override this data.</p>
|
18
|
+
|
19
|
+
<form method="POST">
|
20
|
+
<div class="row">
|
21
|
+
<div class="col-md-6">
|
22
|
+
{% for regex in metadata_extraction_regexes %}
|
23
|
+
<div class="mb-3">
|
24
|
+
<label for="regex_{{ loop.index }}" class="form-label">Regular expression</label>
|
25
|
+
<input type="text" class="form-control" id="regex_{{ loop.index }}" name="regex" value="{{ regex }}" />
|
26
|
+
</div>
|
27
|
+
{% endfor %}
|
28
|
+
<div class="mb-3">
|
29
|
+
<label for="regex_new" class="form-label">Regular expression</label>
|
30
|
+
<input type="text" class="form-control" id="regex_new" name="regex" />
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<div class="col-md-6">
|
35
|
+
<p>To give an example of what is possible, consider a directory structure where we have
|
36
|
+
<tt>{kind}/{equipment}/{date} {name}.{ext}</tt>. Such an activity could be <tt>Ride/Red
|
37
|
+
Roadbike/2024-08-10 11-45-00 Ride with Friends.fit</tt>. In order to extract this, we could use the
|
38
|
+
following regular expression:
|
39
|
+
</p>
|
40
|
+
|
41
|
+
<div class="code">
|
42
|
+
<pre
|
43
|
+
class="code literal-block">(?P<kind>[^/]+)/(?P<equipment>[^/]+)/[-\d_ .]+(?P<name>[^/\.]+)</pre>
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<p>This uses "capture groups". Have a look at the <a href="https://docs.python.org/3/library/re.html">Python
|
47
|
+
regex documentation</a> if you want to build your own.</p>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
|
51
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
52
|
+
</form>
|
53
|
+
|
54
|
+
|
55
|
+
{% endblock %}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<h1 class="mb-3">Privacy Zones</h1>
|
6
|
+
|
7
|
+
<p>You might want to remove points that are close to your home, work or relatives. For this you can define
|
8
|
+
arbitrary polygons as "privacy zones". Go to <a href="https://geojson.io/" target="_blank">GeoJSON.io</a> to have a
|
9
|
+
nice interactive tool for creating overlays. Create a single polygon, rectangle or circle to define a privacy zone.
|
10
|
+
You can define as many zones as you want, but each zone must only consist of a single area. When you are done, copy
|
11
|
+
the JSON output from the left into the text box below.</p>
|
12
|
+
|
13
|
+
<p>You can change the name on an existing privacy zone. To delete a zone, delete the name or the GeoJSON. Zones without
|
14
|
+
a name will be deleted.</p>
|
15
|
+
|
16
|
+
<script>
|
17
|
+
function add_map(id, geojson) {
|
18
|
+
let map = L.map(`map-${id}`, {
|
19
|
+
fullscreenControl: true
|
20
|
+
})
|
21
|
+
L.tileLayer('/tile/color/{z}/{x}/{y}.png', {
|
22
|
+
maxZoom: 19,
|
23
|
+
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
24
|
+
}).addTo(map)
|
25
|
+
|
26
|
+
let geojson_layer = L.geoJSON(geojson).addTo(map)
|
27
|
+
map.fitBounds(geojson_layer.getBounds())
|
28
|
+
return map
|
29
|
+
}
|
30
|
+
</script>
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
<form method="POST">
|
35
|
+
{% if privacy_zones %}
|
36
|
+
<h2 class="mb-3">Existing zones</h2>
|
37
|
+
|
38
|
+
<div class="row row-cols-1 row-cols-md-3 g-4 mb-3">
|
39
|
+
{% for zone_name, zone_geojson in privacy_zones.items() %}
|
40
|
+
|
41
|
+
<div class="col-md-4">
|
42
|
+
<div class="mb-3">
|
43
|
+
<label for="zone_name_{{ loop.index }}" class="form-label">Name</label>
|
44
|
+
<input type="text" class="form-control" id="zone_name_{{ loop.index }}" name="zone_name"
|
45
|
+
value="{{ zone_name }}" />
|
46
|
+
</div>
|
47
|
+
<div class="mb-3">
|
48
|
+
<label for="zone_geojson_{{ loop.index }}" class="form-label">GeoJSON</label>
|
49
|
+
<textarea class="form-control" id="zone_geojson_{{ loop.index }}" name="zone_geojson"
|
50
|
+
rows="10">{{ zone_geojson|tojson(indent=2) }}</textarea>
|
51
|
+
</div>
|
52
|
+
<div class="card-img-top" id="map-{{ loop.index }}" style="height: 300px; width: 100%;"></div>
|
53
|
+
<script>
|
54
|
+
let map{{ loop.index }} = add_map("{{ loop.index }}", {{ zone_geojson | safe }})
|
55
|
+
</script>
|
56
|
+
</div>
|
57
|
+
{% endfor %}
|
58
|
+
</div>
|
59
|
+
{% endif %}
|
60
|
+
|
61
|
+
<h2 class="mb-3">New zone</h2>
|
62
|
+
|
63
|
+
<div class="row mb-3">
|
64
|
+
<div class="col-md-4">
|
65
|
+
<div class="mb-3">
|
66
|
+
<label for="new_zone_name" class="form-label">Name</label>
|
67
|
+
<input type="text" class="form-control" id="new_zone_name" name="zone_name" />
|
68
|
+
</div>
|
69
|
+
<div class="mb-3">
|
70
|
+
<label for="new_zone_geojson" class="form-label">GeoJSON</label>
|
71
|
+
<textarea class="form-control" id="new_zone_geojson" name="zone_geojson" rows="10"></textarea>
|
72
|
+
</div>
|
73
|
+
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
|
77
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
78
|
+
</form>
|
79
|
+
|
80
|
+
|
81
|
+
{% endblock %}
|
@@ -13,21 +13,31 @@
|
|
13
13
|
button. If you don't want to share the API limits with other people, go the <a
|
14
14
|
href="https://www.strava.com/settings/api">Strava API page</a> and create your own app. Then fill in
|
15
15
|
"client ID" and "client secret" in the form.</p>
|
16
|
+
|
17
|
+
<p>
|
18
|
+
Status:
|
19
|
+
{% if strava_client_code %}
|
20
|
+
<span class="badge text-bg-success">Connected</span>
|
21
|
+
{% else %}
|
22
|
+
<span class="badge text-bg-secondary">Not connected</span>
|
23
|
+
{% endif %}
|
24
|
+
</p>
|
16
25
|
</div>
|
17
26
|
|
18
27
|
<div class="col-md-6">
|
19
|
-
<form action="
|
28
|
+
<form action="" method="POST">
|
20
29
|
<div class="mb-3">
|
21
|
-
<label for="
|
22
|
-
<input type="text" class="form-control" id="
|
30
|
+
<label for="strava_client_id" class="form-label">Client ID</label>
|
31
|
+
<input type="text" class="form-control" id="strava_client_id" name="strava_client_id"
|
32
|
+
value="{{ strava_client_id }}" />
|
23
33
|
</div>
|
24
34
|
<div class="mb-3">
|
25
|
-
<label for="
|
26
|
-
<input type="text" class="form-control" id="
|
27
|
-
value="
|
35
|
+
<label for="strava_client_secret" class="form-label">Client Secret</label>
|
36
|
+
<input type="text" class="form-control" id="strava_client_secret" name="strava_client_secret"
|
37
|
+
value="{{ strava_client_secret }}" />
|
28
38
|
</div>
|
29
39
|
|
30
|
-
<
|
40
|
+
<button type="submit" class="btn btn-primary">Connect to Strava</button>
|
31
41
|
</form>
|
32
42
|
</div>
|
33
43
|
</div>
|
@@ -60,6 +60,7 @@
|
|
60
60
|
</button>
|
61
61
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
62
62
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
63
|
+
|
63
64
|
{% if num_activities > 0 %}
|
64
65
|
<li class="nav-item">
|
65
66
|
<a class="nav-link" aria-current="page" href="{{ url_for('summary.index') }}">Summary</a>
|
@@ -91,7 +92,10 @@
|
|
91
92
|
<a class="nav-link" aria-current="page" href="{{ url_for('upload.index') }}">Upload</a>
|
92
93
|
</li>
|
93
94
|
<li class="nav-item">
|
94
|
-
<a class="nav-link" aria-current="page" href="{{ url_for('
|
95
|
+
<a class="nav-link" aria-current="page" href="{{ url_for('upload.reload') }}">Refresh</a>
|
96
|
+
</li>
|
97
|
+
<li class="nav-item">
|
98
|
+
<a class="nav-link" aria-current="page" href="{{ url_for('settings.index') }}">Settings</a>
|
95
99
|
</li>
|
96
100
|
|
97
101
|
|
@@ -4,12 +4,13 @@ from flask import render_template
|
|
4
4
|
from ...core.activities import ActivityRepository
|
5
5
|
from ...explorer.tile_visits import TileVisitAccessor
|
6
6
|
from .controller import UploadController
|
7
|
+
from geo_activity_playground.core.config import Config
|
7
8
|
|
8
9
|
|
9
10
|
def make_upload_blueprint(
|
10
11
|
repository: ActivityRepository,
|
11
12
|
tile_visit_accessor: TileVisitAccessor,
|
12
|
-
config:
|
13
|
+
config: Config,
|
13
14
|
) -> Blueprint:
|
14
15
|
blueprint = Blueprint("upload", __name__, template_folder="templates")
|
15
16
|
|
@@ -25,4 +26,12 @@ def make_upload_blueprint(
|
|
25
26
|
def receive():
|
26
27
|
return upload_controller.receive()
|
27
28
|
|
29
|
+
@blueprint.route("/refresh")
|
30
|
+
def reload():
|
31
|
+
return render_template("upload/reload.html.j2")
|
32
|
+
|
33
|
+
@blueprint.route("/execute-reload")
|
34
|
+
def execute_reload():
|
35
|
+
return upload_controller.execute_reload()
|
36
|
+
|
28
37
|
return blueprint
|
@@ -1,18 +1,18 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
import pathlib
|
4
|
-
import sys
|
5
4
|
|
6
5
|
from flask import flash
|
7
6
|
from flask import redirect
|
8
7
|
from flask import request
|
9
8
|
from flask import Response
|
9
|
+
from flask import url_for
|
10
10
|
from werkzeug.utils import secure_filename
|
11
11
|
|
12
12
|
from geo_activity_playground.core.activities import ActivityRepository
|
13
13
|
from geo_activity_playground.core.activities import build_activity_meta
|
14
|
+
from geo_activity_playground.core.config import Config
|
14
15
|
from geo_activity_playground.core.enrichment import enrich_activities
|
15
|
-
from geo_activity_playground.core.paths import _strava_dynamic_config_path
|
16
16
|
from geo_activity_playground.explorer.tile_visits import compute_tile_evolution
|
17
17
|
from geo_activity_playground.explorer.tile_visits import compute_tile_visits
|
18
18
|
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
@@ -32,7 +32,7 @@ class UploadController:
|
|
32
32
|
self,
|
33
33
|
repository: ActivityRepository,
|
34
34
|
tile_visit_accessor: TileVisitAccessor,
|
35
|
-
config:
|
35
|
+
config: Config,
|
36
36
|
) -> None:
|
37
37
|
self._repository = repository
|
38
38
|
self._tile_visit_accessor = tile_visit_accessor
|
@@ -45,7 +45,7 @@ class UploadController:
|
|
45
45
|
directories.sort()
|
46
46
|
return {
|
47
47
|
"directories": directories,
|
48
|
-
"has_upload":
|
48
|
+
"has_upload": self._config.upload_password,
|
49
49
|
}
|
50
50
|
|
51
51
|
def receive(self) -> Response:
|
@@ -54,7 +54,7 @@ class UploadController:
|
|
54
54
|
flash("No file could be found. Did you select a file?", "warning")
|
55
55
|
return redirect("/upload")
|
56
56
|
|
57
|
-
if request.form["password"] != self._config
|
57
|
+
if request.form["password"] != self._config.upload_password:
|
58
58
|
flash("Incorrect upload password!", "danger")
|
59
59
|
return redirect("/upload")
|
60
60
|
|
@@ -88,24 +88,37 @@ class UploadController:
|
|
88
88
|
flash(f"Activity was saved with ID {activity_id}.", "success")
|
89
89
|
return redirect(f"/activity/{activity_id}")
|
90
90
|
|
91
|
+
def execute_reload(self) -> None:
|
92
|
+
scan_for_activities(
|
93
|
+
self._repository,
|
94
|
+
self._tile_visit_accessor,
|
95
|
+
self._config,
|
96
|
+
skip_strava=True,
|
97
|
+
)
|
98
|
+
flash("Scanned for new activities.", category="success")
|
99
|
+
return redirect(url_for("index"))
|
100
|
+
|
91
101
|
|
92
102
|
def scan_for_activities(
|
93
103
|
repository: ActivityRepository,
|
94
104
|
tile_visit_accessor: TileVisitAccessor,
|
95
|
-
config:
|
105
|
+
config: Config,
|
96
106
|
skip_strava: bool = False,
|
97
107
|
) -> None:
|
98
108
|
if pathlib.Path("Activities").exists():
|
99
|
-
import_from_directory(
|
109
|
+
import_from_directory(
|
110
|
+
config.metadata_extraction_regexes,
|
111
|
+
None,
|
112
|
+
)
|
100
113
|
if pathlib.Path("Strava Export").exists():
|
101
114
|
import_from_strava_checkout()
|
102
|
-
if
|
103
|
-
import_from_strava_api()
|
115
|
+
if config.strava_client_code and not skip_strava:
|
116
|
+
import_from_strava_api(config)
|
104
117
|
|
105
|
-
enrich_activities(config
|
118
|
+
enrich_activities(config)
|
106
119
|
build_activity_meta()
|
107
120
|
repository.reload()
|
108
121
|
|
109
122
|
if len(repository) > 0:
|
110
123
|
compute_tile_visits(repository, tile_visit_accessor)
|
111
|
-
compute_tile_evolution(tile_visit_accessor)
|
124
|
+
compute_tile_evolution(tile_visit_accessor, config)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
<h1>Reloading</h1>
|
5
|
+
|
6
|
+
<p>Checking for new activities, importing them and adding them to the database. This might take a while …</p>
|
7
|
+
|
8
|
+
<div class="progress" role="progressbar" aria-label="Animated striped example" aria-valuenow="100" aria-valuemin="0"
|
9
|
+
aria-valuemax="100">
|
10
|
+
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<script type="text/javascript">
|
14
|
+
window.location.replace("{{ url_for('.execute_reload') }}");
|
15
|
+
</script>
|
16
|
+
{% endblock %}
|
{geo_activity_playground-0.26.3.dist-info → geo_activity_playground-0.27.0.dist-info}/RECORD
RENAMED
@@ -1,39 +1,42 @@
|
|
1
1
|
geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
geo_activity_playground/__main__.py,sha256=
|
2
|
+
geo_activity_playground/__main__.py,sha256=0feB3cw3-U9q0Dkrn5co76gTPpwGRWyfI5mfWNSZ_lU,4063
|
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=Z81Itv3qrokxE1-YnPrDJ0QteyBZYjQYvTP-tHvR9j4,6606
|
5
|
+
geo_activity_playground/core/config.py,sha256=Zk3j-MwRbkFyCN4e4jLdzA7yRNLFEWu0Uau78TBftiE,4417
|
6
6
|
geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
|
7
|
-
geo_activity_playground/core/enrichment.py,sha256=
|
7
|
+
geo_activity_playground/core/enrichment.py,sha256=CwZhW-svgPAYbdx3n9kvKlTgcsiCaeuJfSRCC4JxX6g,7411
|
8
|
+
geo_activity_playground/core/heart_rate.py,sha256=IwMt58TpjOYqpAxtsj07zP2ttpN_J3GZeiv-qGhYyJc,1598
|
8
9
|
geo_activity_playground/core/heatmap.py,sha256=bRLQHzmTEsQbX8XWeg85x_lRGk272UoYRiCnoxZ5da0,4189
|
9
|
-
geo_activity_playground/core/paths.py,sha256=
|
10
|
+
geo_activity_playground/core/paths.py,sha256=AiYUJv46my_FGYbHZmSs5ZrqeE65GNdWEMmXZgunZrk,2150
|
10
11
|
geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
|
11
12
|
geo_activity_playground/core/similarity.py,sha256=Jo8jRViuORCxdIGvyaflgsQhwu9S_jn10a450FRL18A,3159
|
12
|
-
geo_activity_playground/core/tasks.py,sha256=
|
13
|
+
geo_activity_playground/core/tasks.py,sha256=k_DHzY--V5EHqMh-aNFh-VsLiQntvAsGbmK5zZJ0YPI,2909
|
13
14
|
geo_activity_playground/core/test_tiles.py,sha256=zce1FxNfsSpOQt66jMehdQRVoNdl-oiFydx6iVBHZXM,764
|
14
15
|
geo_activity_playground/core/test_time_conversion.py,sha256=Sh6nZA3uCTOdZTZa3yOijtR0m74QtZu2mcWXsDNnyQI,984
|
15
|
-
geo_activity_playground/core/tiles.py,sha256=
|
16
|
+
geo_activity_playground/core/tiles.py,sha256=KpzD-h3kNzZ2ieLt6f2xHilSF3lHyfaEXPnrGvlIAz0,3379
|
16
17
|
geo_activity_playground/core/time_conversion.py,sha256=9J6aTlqJhWvsknQkoECNL-CIG-8BKs6ZatJJ9XJnTsg,367
|
17
18
|
geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
19
|
geo_activity_playground/explorer/grid_file.py,sha256=k6j6KBEk2a2BY-onE8SV5TJsERGGyOrlY4as__meWpA,3304
|
19
|
-
geo_activity_playground/explorer/tile_visits.py,sha256=
|
20
|
+
geo_activity_playground/explorer/tile_visits.py,sha256=p0MK0V4fHhroR-FN8S9m5nW0vT7uO39Euo-_mOw0Q8k,13586
|
20
21
|
geo_activity_playground/explorer/video.py,sha256=ROAmV9shfJyqTgnXVD41KFORiwnRgVpEWenIq4hMCRM,4389
|
21
22
|
geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
23
|
geo_activity_playground/importers/activity_parsers.py,sha256=m2SpvGlTZ8F3gG6YB24_ZFrlOAbtqbfWi-GIYspeUco,10593
|
23
|
-
geo_activity_playground/importers/
|
24
|
-
geo_activity_playground/importers/
|
25
|
-
geo_activity_playground/importers/
|
24
|
+
geo_activity_playground/importers/csv_parser.py,sha256=7x0U4MyKNCsVFnXU6US8FQ28X9PTIoQS8U_hzF5l4So,2147
|
25
|
+
geo_activity_playground/importers/directory.py,sha256=_KEoRxmjz4-oVhJgcMnA_JUO_ERHEkedqV5TPvANSEk,5735
|
26
|
+
geo_activity_playground/importers/strava_api.py,sha256=pgWZp-cWOLkvlDE85lTiEKM8BCZYzOlAAdKoa2F7c6o,7780
|
27
|
+
geo_activity_playground/importers/strava_checkout.py,sha256=N-uGTkhBJMC7cPYjRRXHOSLwpK3wc6aaSrY2RQfSitA,9419
|
28
|
+
geo_activity_playground/importers/test_csv_parser.py,sha256=LXqva7GuSAfXYE2zZQrg-69lCtfy5MxLSq6BRwL_VyI,1191
|
26
29
|
geo_activity_playground/importers/test_directory.py,sha256=ljXokx7q0OgtHvEdHftcQYEmZJUDVv3OOF5opklxdT4,724
|
27
30
|
geo_activity_playground/importers/test_strava_api.py,sha256=4vX7wDr1a9aRh8myxNrIq6RwDBbP8ZeoXXPc10CAbW4,431
|
28
31
|
geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
32
|
geo_activity_playground/webui/activity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
|
-
geo_activity_playground/webui/activity/blueprint.py,sha256=
|
31
|
-
geo_activity_playground/webui/activity/controller.py,sha256=
|
33
|
+
geo_activity_playground/webui/activity/blueprint.py,sha256=upQzZa5sKApj_Fmu6PziFDboi7SBL5Zsi-tNSSNPlEE,1759
|
34
|
+
geo_activity_playground/webui/activity/controller.py,sha256=VJrbUD2oofESV2imNr7LBrx6jhSA8miuwnrERPsUuQc,17573
|
32
35
|
geo_activity_playground/webui/activity/templates/activity/day.html.j2,sha256=r3qKl9uTzOko4R-ZzyYAZt1j61JSevYP4g0Yi06HHPg,2702
|
33
36
|
geo_activity_playground/webui/activity/templates/activity/lines.html.j2,sha256=5gB1aDjRgi_RventenRfC10_FtMT4ch_VuWvA9AMlBY,1121
|
34
37
|
geo_activity_playground/webui/activity/templates/activity/name.html.j2,sha256=RDLEt6ip8_ngmdLgaC5jg92Dk-F2umGwKkd8cWmvVko,2400
|
35
|
-
geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=
|
36
|
-
geo_activity_playground/webui/app.py,sha256=
|
38
|
+
geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=3g-Hab06uWBPJo6DmTk4lM2OlozE1EyrQK6RgTgTuwU,5521
|
39
|
+
geo_activity_playground/webui/app.py,sha256=xKPP0WDyHMAbkpJsuEz53skIjOMRBMvueD6LpRdYJE0,4119
|
37
40
|
geo_activity_playground/webui/calendar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
38
41
|
geo_activity_playground/webui/calendar/blueprint.py,sha256=rlnhgU2DWAcdLMRq7m77NzrM_aDyp4s3kuuQHuzjHhg,782
|
39
42
|
geo_activity_playground/webui/calendar/controller.py,sha256=QpSAkR2s1sbLSu6P_fNNTccgGglOzEH2PIv1XwKxeVY,2778
|
@@ -41,22 +44,31 @@ geo_activity_playground/webui/calendar/templates/calendar/index.html.j2,sha256=x
|
|
41
44
|
geo_activity_playground/webui/calendar/templates/calendar/month.html.j2,sha256=sRIiNo_Rp9CHary6e-lnpKJKOuAonoDEBvKMxzbTLQE,1802
|
42
45
|
geo_activity_playground/webui/eddington/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
43
46
|
geo_activity_playground/webui/eddington/blueprint.py,sha256=evIvueLfDWVTxJ9pRguqmZ9-Pybd2WmBRst_-7vX2QA,551
|
44
|
-
geo_activity_playground/webui/eddington/controller.py,sha256=
|
47
|
+
geo_activity_playground/webui/eddington/controller.py,sha256=ly7JSkSS79kO4CL_xugB62uRuuWKVqOjbN-pheelv94,2910
|
45
48
|
geo_activity_playground/webui/eddington/templates/eddington/index.html.j2,sha256=XHKeUymQMS5x00PLOVlg-nSRCz_jHB2pvD8QunULWJ4,1839
|
46
49
|
geo_activity_playground/webui/entry_controller.py,sha256=n9v4MriyL8kDR91LE9eeqc2tAvxyzFgoNMMXpr0qh4g,1906
|
47
50
|
geo_activity_playground/webui/equipment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
48
|
-
geo_activity_playground/webui/equipment/blueprint.py,sha256=
|
49
|
-
geo_activity_playground/webui/equipment/controller.py,sha256=
|
51
|
+
geo_activity_playground/webui/equipment/blueprint.py,sha256=_NIhRJuJNbXpEd_nEPo01AqnUqPgo1vawFn7E3yoeng,636
|
52
|
+
geo_activity_playground/webui/equipment/controller.py,sha256=_iLkQeeu7UYMg7FGNemTrhm8e0pf409f9aUXnGfNsCI,4022
|
50
53
|
geo_activity_playground/webui/equipment/templates/equipment/index.html.j2,sha256=FEfxB4XwVYELAOdjVlSlprjJH_kLmE-pNWEEXdPqc6I,1778
|
51
54
|
geo_activity_playground/webui/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
52
|
-
geo_activity_playground/webui/explorer/blueprint.py,sha256=
|
53
|
-
geo_activity_playground/webui/explorer/controller.py,sha256=
|
54
|
-
geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=
|
55
|
+
geo_activity_playground/webui/explorer/blueprint.py,sha256=EKnBs8llqT6Wy1uac18dF2epp3TebF9p3iGlSbj6Vl0,2337
|
56
|
+
geo_activity_playground/webui/explorer/controller.py,sha256=M3FdPLrcP9GZiCjuRVY0ug1DtTqLvIbEDCzRHf6QuOE,11555
|
57
|
+
geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=cm9pWY0vB84DtkTH-LBvSzfLU1FnmxQ2ECyw3Bl7dTo,6945
|
55
58
|
geo_activity_playground/webui/heatmap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
59
|
geo_activity_playground/webui/heatmap/blueprint.py,sha256=bjQu-HL3QN5UpJ6tHOifhcLGlPr_hIKvaRu78md4JqM,1470
|
57
60
|
geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=i376qEqTJrhezGsdPyKR8VsRY2N0JdEra7_kY7NmwW8,6822
|
58
61
|
geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2,sha256=YLeu6P4djl8G4qAXR6DhetseqrbOodN7aN4coocknc4,1875
|
59
62
|
geo_activity_playground/webui/search_controller.py,sha256=PzMf7b8tiJKZIZoPvQ9A2hOrzoKV9cS3jq05w2fK94c,532
|
63
|
+
geo_activity_playground/webui/settings/blueprint.py,sha256=1u16oRlauOSPVBGBzjf08Ja9jxbqEKnwcDISWl7CmJU,4113
|
64
|
+
geo_activity_playground/webui/settings/controller.py,sha256=ixuYgpMGggg9kmcIVTgL2gzAPhYjMpqWEPxS7YXlmnA,7902
|
65
|
+
geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2,sha256=ltaYwFe8S8Mi72ddmIp1vwqlu8MEXXjBGfbpN2WBTC4,1728
|
66
|
+
geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2,sha256=UPT3MegRgSeff36lhCo0l3ZwhqNSIg5gM6h2s32GkCY,4255
|
67
|
+
geo_activity_playground/webui/settings/templates/settings/index.html.j2,sha256=26VEC_MbdIyd7vbFokDFtJelcuK3YFb1_3WaFLGv_bk,3183
|
68
|
+
geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2,sha256=IdUfXon1Pu8zX3NirKb28ypshLHOvZRpz2T4bJrzrak,1067
|
69
|
+
geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2,sha256=Ppa8O-zRJznbeCsF4YQj37_HM9nOW8fyTi66jvWvHmA,2285
|
70
|
+
geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2,sha256=7BxFvCaVJOEqbImyK5vxCmhh-NGSFaRa9ARhqjZeYJ0,3093
|
71
|
+
geo_activity_playground/webui/settings/templates/settings/strava.html.j2,sha256=FrXgT-m1PgvsQWo9kMKpk8QenKeifSDBCZFqKgsHRxQ,1827
|
60
72
|
geo_activity_playground/webui/square_planner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
61
73
|
geo_activity_playground/webui/square_planner/blueprint.py,sha256=r2VkSM547chX85g6c1BQ8NC-tkdqGdYp-2ZALBiiDTc,1320
|
62
74
|
geo_activity_playground/webui/square_planner/controller.py,sha256=wYcNEviDgqyYxSrnwMD_5LnYXIazVH9plGX8RxG6oco,3464
|
@@ -73,28 +85,23 @@ geo_activity_playground/webui/static/favicon.ico,sha256=uVNZlrF22zn1lcCS_9wAoWLh
|
|
73
85
|
geo_activity_playground/webui/static/mstile-150x150.png,sha256=j1ANUQJ1Xi1DR2sGqYZztob2ypfGw04eNtGpN9SxExA,11964
|
74
86
|
geo_activity_playground/webui/static/safari-pinned-tab.svg,sha256=OzoEVGY0igWRXM1NiM3SRKugdICBN7aB_XuxaC3Mu9Q,8371
|
75
87
|
geo_activity_playground/webui/static/site.webmanifest,sha256=4vYxdPMpwTdB8EmOvHkkYcjZ8Yrci3pOwwY3o_VwACA,440
|
76
|
-
geo_activity_playground/webui/strava/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
77
|
-
geo_activity_playground/webui/strava/blueprint.py,sha256=Q2Wfxlg7bR2vjWZ3UpckPl9jWHwNB8hfP7SLBufgiUY,1055
|
78
|
-
geo_activity_playground/webui/strava/controller.py,sha256=Ye0R-nu5SwFdk2DAIEq7OE76XLgnRk9y8iJjLObbAUc,1437
|
79
|
-
geo_activity_playground/webui/strava/templates/strava/client-id.html.j2,sha256=m5gJk0VxcIn0VcROsOke5A5MbWXC5O1e1Lxc09MyN_g,1493
|
80
|
-
geo_activity_playground/webui/strava/templates/strava/connected.html.j2,sha256=TN6H8YPjG2zk8fodTaSaloTEJCXnpgTCIRX_uEVTiNI,274
|
81
88
|
geo_activity_playground/webui/summary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
82
89
|
geo_activity_playground/webui/summary/blueprint.py,sha256=kzQ6MDOycQKfDcVoEUmL7HYHJA_gu8DlzVHwO37-_jA,514
|
83
90
|
geo_activity_playground/webui/summary/controller.py,sha256=ZOrwfrKjpc8hecUYImBvesKXZi06obfR1yhQkVTeWzw,8981
|
84
91
|
geo_activity_playground/webui/summary/templates/summary/index.html.j2,sha256=rsII1eMY-xNugh8A9SecnEcDZqkEOWYIfiHAGroQYuM,4442
|
85
92
|
geo_activity_playground/webui/templates/home.html.j2,sha256=fp48MjBuO4QJfQz6YPOWH56IzStgaclx9XbwEKmUFHQ,2403
|
86
|
-
geo_activity_playground/webui/templates/page.html.j2,sha256=
|
93
|
+
geo_activity_playground/webui/templates/page.html.j2,sha256=sqerdJLOIcgMtQy533kHPD9uVIxF3v6ItLQSjuoCqlI,9589
|
87
94
|
geo_activity_playground/webui/templates/search.html.j2,sha256=FvNRoDfUlSzXjM_tqZY_fDhuhUDgbPaY73q56gdvF1A,1130
|
88
|
-
geo_activity_playground/webui/templates/settings.html.j2,sha256=-q9GflrG2t6kSt-NerBN5NV1gRp12JT3KgGp_aPaKT8,674
|
89
95
|
geo_activity_playground/webui/tile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
90
96
|
geo_activity_playground/webui/tile/blueprint.py,sha256=cK0o2Z3BrLycgF9zw0F8s9qF-JaYDbF5Gog-GXDtUZ8,943
|
91
97
|
geo_activity_playground/webui/tile/controller.py,sha256=PISh4vKs27b-LxFfTARtr5RAwHFresA1Kw1MDcERSRU,1221
|
92
98
|
geo_activity_playground/webui/upload/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
93
|
-
geo_activity_playground/webui/upload/blueprint.py,sha256=
|
94
|
-
geo_activity_playground/webui/upload/controller.py,sha256=
|
99
|
+
geo_activity_playground/webui/upload/blueprint.py,sha256=qU6ZKOrhB8DA8GSbOECVu2fPKd2VtBFp0mEFzjJAgIA,1083
|
100
|
+
geo_activity_playground/webui/upload/controller.py,sha256=NQ-JOT-kP5Vk0wgjP0AQP6E7HFAVHnCymlKhVnhwDYo,4255
|
95
101
|
geo_activity_playground/webui/upload/templates/upload/index.html.j2,sha256=hfXkEXaz_MkM46rU56423D6V6WtK-EAXPszSY-_-Tx8,1471
|
96
|
-
geo_activity_playground
|
97
|
-
geo_activity_playground-0.
|
98
|
-
geo_activity_playground-0.
|
99
|
-
geo_activity_playground-0.
|
100
|
-
geo_activity_playground-0.
|
102
|
+
geo_activity_playground/webui/upload/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
|
103
|
+
geo_activity_playground-0.27.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
|
104
|
+
geo_activity_playground-0.27.0.dist-info/METADATA,sha256=2EqcALnLKO3K5mS5TFu-pcMNfJYpP-G1b57QPk7oi7Q,1665
|
105
|
+
geo_activity_playground-0.27.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
106
|
+
geo_activity_playground-0.27.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
|
107
|
+
geo_activity_playground-0.27.0.dist-info/RECORD,,
|
File without changes
|
@@ -1,33 +0,0 @@
|
|
1
|
-
from flask import Blueprint
|
2
|
-
from flask import redirect
|
3
|
-
from flask import render_template
|
4
|
-
from flask import request
|
5
|
-
|
6
|
-
from .controller import StravaController
|
7
|
-
|
8
|
-
|
9
|
-
def make_strava_blueprint(host: str, port: int) -> Blueprint:
|
10
|
-
strava_controller = StravaController(host, port)
|
11
|
-
blueprint = Blueprint("strava", __name__, template_folder="templates")
|
12
|
-
|
13
|
-
@blueprint.route("/setup")
|
14
|
-
def setup():
|
15
|
-
return render_template(
|
16
|
-
"strava/client-id.html.j2", **strava_controller.set_client_id()
|
17
|
-
)
|
18
|
-
|
19
|
-
@blueprint.route("/post-client-id", methods=["POST"])
|
20
|
-
def post_client_id():
|
21
|
-
client_id = request.form["client_id"]
|
22
|
-
client_secret = request.form["client_secret"]
|
23
|
-
url = strava_controller.save_client_id(client_id, client_secret)
|
24
|
-
return redirect(url)
|
25
|
-
|
26
|
-
@blueprint.route("/callback")
|
27
|
-
def strava_callback():
|
28
|
-
code = request.args.get("code", type=str)
|
29
|
-
return render_template(
|
30
|
-
"strava/connected.html.j2", **strava_controller.save_code(code)
|
31
|
-
)
|
32
|
-
|
33
|
-
return blueprint
|