geo-activity-playground 0.41.0__py3-none-any.whl → 0.43.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/alembic/script.py.mako +0 -6
- geo_activity_playground/alembic/versions/da2cba03b71d_add_photos.py +40 -0
- geo_activity_playground/alembic/versions/script.py.mako +6 -0
- geo_activity_playground/core/activities.py +7 -15
- geo_activity_playground/core/datamodel.py +91 -85
- geo_activity_playground/core/enrichment.py +15 -6
- geo_activity_playground/core/meta_search.py +78 -34
- geo_activity_playground/core/missing_values.py +16 -0
- geo_activity_playground/core/paths.py +2 -0
- geo_activity_playground/core/test_missing_values.py +19 -0
- geo_activity_playground/explorer/tile_visits.py +1 -1
- geo_activity_playground/webui/app.py +22 -8
- geo_activity_playground/webui/blueprints/activity_blueprint.py +18 -10
- geo_activity_playground/webui/blueprints/photo_blueprint.py +198 -0
- geo_activity_playground/webui/blueprints/settings_blueprint.py +32 -0
- geo_activity_playground/webui/search_util.py +23 -7
- geo_activity_playground/webui/templates/activity/edit.html.j2 +15 -0
- geo_activity_playground/webui/templates/activity/show.html.j2 +56 -12
- geo_activity_playground/webui/templates/eddington/distance.html.j2 +1 -2
- geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +1 -2
- geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +18 -15
- geo_activity_playground/webui/templates/heatmap/index.html.j2 +1 -2
- geo_activity_playground/webui/templates/page.html.j2 +8 -0
- geo_activity_playground/webui/templates/photo/map.html.j2 +45 -0
- geo_activity_playground/webui/templates/photo/new.html.j2 +13 -0
- geo_activity_playground/webui/templates/search/index.html.j2 +1 -2
- geo_activity_playground/webui/templates/search_form.html.j2 +47 -22
- geo_activity_playground/webui/templates/settings/index.html.j2 +9 -0
- geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +17 -0
- geo_activity_playground/webui/templates/settings/tags-list.html.j2 +19 -0
- geo_activity_playground/webui/templates/settings/tags-new.html.j2 +17 -0
- geo_activity_playground/webui/templates/summary/index.html.j2 +12 -10
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/METADATA +3 -1
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/RECORD +37 -28
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/WHEEL +1 -1
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
from typing import Sequence
|
2
|
+
from typing import Union
|
3
|
+
|
4
|
+
import sqlalchemy as sa
|
5
|
+
from alembic import op
|
6
|
+
|
7
|
+
|
8
|
+
# revision identifiers, used by Alembic.
|
9
|
+
revision: str = "da2cba03b71d"
|
10
|
+
down_revision: Union[str, None] = "38882503dc7c"
|
11
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
12
|
+
depends_on: Union[str, Sequence[str], None] = None
|
13
|
+
|
14
|
+
|
15
|
+
def upgrade() -> None:
|
16
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
17
|
+
op.create_table(
|
18
|
+
"photos",
|
19
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
20
|
+
sa.Column("filename", sa.String(), nullable=False),
|
21
|
+
sa.Column("time", sa.DateTime(), nullable=False),
|
22
|
+
sa.Column("latitude", sa.Float(), nullable=False),
|
23
|
+
sa.Column("longitude", sa.Float(), nullable=False),
|
24
|
+
sa.Column("activity_id", sa.Integer(), nullable=False),
|
25
|
+
sa.ForeignKeyConstraint(["activity_id"], ["activities.id"], name="activity_id"),
|
26
|
+
sa.PrimaryKeyConstraint("id"),
|
27
|
+
)
|
28
|
+
with op.batch_alter_table("tags", schema=None) as batch_op:
|
29
|
+
batch_op.create_unique_constraint("tags_tag", ["tag"])
|
30
|
+
|
31
|
+
# ### end Alembic commands ###
|
32
|
+
|
33
|
+
|
34
|
+
def downgrade() -> None:
|
35
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
36
|
+
with op.batch_alter_table("tags", schema=None) as batch_op:
|
37
|
+
batch_op.drop_constraint("tags_tag", type_="unique")
|
38
|
+
|
39
|
+
op.drop_table("photos")
|
40
|
+
# ### end Alembic commands ###
|
@@ -2,6 +2,7 @@ import datetime
|
|
2
2
|
import functools
|
3
3
|
import logging
|
4
4
|
from collections.abc import Callable
|
5
|
+
from collections.abc import Sequence
|
5
6
|
from typing import Any
|
6
7
|
from typing import Optional
|
7
8
|
|
@@ -21,20 +22,11 @@ from geo_activity_playground.core.datamodel import query_activity_meta
|
|
21
22
|
logger = logging.getLogger(__name__)
|
22
23
|
|
23
24
|
|
24
|
-
def make_activity_meta() -> ActivityMeta:
|
25
|
-
return ActivityMeta(
|
26
|
-
calories=None,
|
27
|
-
commute=False,
|
28
|
-
consider_for_achievements=True,
|
29
|
-
equipment="Unknown",
|
30
|
-
kind="Unknown",
|
31
|
-
steps=None,
|
32
|
-
)
|
33
|
-
|
34
|
-
|
35
25
|
class ActivityRepository:
|
36
26
|
def __len__(self) -> int:
|
37
|
-
return
|
27
|
+
return DB.session.scalars(
|
28
|
+
sqlalchemy.select(sqlalchemy.func.count()).select_from(Activity)
|
29
|
+
).one()
|
38
30
|
|
39
31
|
def has_activity(self, activity_id: int) -> bool:
|
40
32
|
return bool(
|
@@ -52,14 +44,14 @@ class ActivityRepository:
|
|
52
44
|
else:
|
53
45
|
return None
|
54
46
|
|
55
|
-
def get_activity_ids(self, only_achievements: bool = False) ->
|
47
|
+
def get_activity_ids(self, only_achievements: bool = False) -> Sequence[int]:
|
56
48
|
query = sqlalchemy.select(Activity.id)
|
57
49
|
if only_achievements:
|
58
50
|
query = query.where(Kind.consider_for_achievements)
|
59
|
-
result = DB.session.scalars(query).all()
|
51
|
+
result = DB.session.scalars(query.order_by(Activity.start)).all()
|
60
52
|
return result
|
61
53
|
|
62
|
-
def iter_activities(self, new_to_old=True, drop_na=False) ->
|
54
|
+
def iter_activities(self, new_to_old=True, drop_na=False) -> Sequence[Activity]:
|
63
55
|
query = sqlalchemy.select(Activity)
|
64
56
|
if drop_na:
|
65
57
|
query = query.where(Activity.start.is_not(None))
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import datetime
|
2
2
|
import json
|
3
3
|
import logging
|
4
|
+
import pathlib
|
4
5
|
from typing import Any
|
5
6
|
from typing import Optional
|
6
7
|
from typing import TypedDict
|
@@ -117,8 +118,9 @@ class Activity(DB.Model):
|
|
117
118
|
secondary=activity_tag_association_table, back_populates="activities"
|
118
119
|
)
|
119
120
|
|
120
|
-
|
121
|
-
|
121
|
+
photos: Mapped[list["Photo"]] = relationship(
|
122
|
+
back_populates="activity", cascade="all, delete-orphan"
|
123
|
+
)
|
122
124
|
|
123
125
|
def __str__(self) -> str:
|
124
126
|
return f"{self.start} {self.name}"
|
@@ -127,11 +129,15 @@ class Activity(DB.Model):
|
|
127
129
|
def average_speed_moving_kmh(self) -> Optional[float]:
|
128
130
|
if self.moving_time:
|
129
131
|
return self.distance_km / (self.moving_time.total_seconds() / 3_600)
|
132
|
+
else:
|
133
|
+
return None
|
130
134
|
|
131
135
|
@property
|
132
136
|
def average_speed_elapsed_kmh(self) -> Optional[float]:
|
133
137
|
if self.elapsed_time:
|
134
138
|
return self.distance_km / (self.elapsed_time.total_seconds() / 3_600)
|
139
|
+
else:
|
140
|
+
return None
|
135
141
|
|
136
142
|
@property
|
137
143
|
def raw_time_series(self) -> pd.DataFrame:
|
@@ -154,38 +160,6 @@ class Activity(DB.Model):
|
|
154
160
|
else:
|
155
161
|
return self.raw_time_series
|
156
162
|
|
157
|
-
def to_dict(self) -> ActivityMeta:
|
158
|
-
equipment = self.equipment.name if self.equipment is not None else "Unknown"
|
159
|
-
kind = self.kind.name if self.kind is not None else "Unknown"
|
160
|
-
consider_for_achievements = (
|
161
|
-
self.kind.consider_for_achievements if self.kind is not None else True
|
162
|
-
)
|
163
|
-
return ActivityMeta(
|
164
|
-
id=self.id,
|
165
|
-
name=self.name,
|
166
|
-
path=self.path,
|
167
|
-
distance_km=self.distance_km,
|
168
|
-
start=self.start,
|
169
|
-
elapsed_time=self.elapsed_time,
|
170
|
-
moving_time=self.moving_time,
|
171
|
-
start_latitude=self.start_latitude,
|
172
|
-
start_longitude=self.start_longitude,
|
173
|
-
end_latitude=self.end_latitude,
|
174
|
-
end_longitude=self.end_longitude,
|
175
|
-
elevation_gain=self.elevation_gain,
|
176
|
-
start_elevation=self.start_elevation,
|
177
|
-
end_elevation=self.end_elevation,
|
178
|
-
calories=self.calories,
|
179
|
-
steps=self.steps,
|
180
|
-
num_new_tiles_14=self.num_new_tiles_14,
|
181
|
-
num_new_tiles_17=self.num_new_tiles_17,
|
182
|
-
equipment=equipment,
|
183
|
-
kind=kind,
|
184
|
-
average_speed_moving_kmh=self.average_speed_moving_kmh,
|
185
|
-
average_speed_elapsed_kmh=self.average_speed_elapsed_kmh,
|
186
|
-
consider_for_achievements=consider_for_achievements,
|
187
|
-
)
|
188
|
-
|
189
163
|
|
190
164
|
class Tag(DB.Model):
|
191
165
|
__tablename__ = "tags"
|
@@ -199,7 +173,18 @@ class Tag(DB.Model):
|
|
199
173
|
)
|
200
174
|
|
201
175
|
|
202
|
-
def
|
176
|
+
def get_or_make_tag(tag: str) -> Tag:
|
177
|
+
tags = DB.session.scalars(sqlalchemy.select(Tag).where(Tag.tag == tag)).all()
|
178
|
+
if tags:
|
179
|
+
assert len(tags) == 1, f"There must be only one tag with name '{tag}'."
|
180
|
+
return tags[0]
|
181
|
+
else:
|
182
|
+
tag = Tag(tag=tag)
|
183
|
+
DB.session.add(tag)
|
184
|
+
return tag
|
185
|
+
|
186
|
+
|
187
|
+
def query_activity_meta(clauses: list = []) -> pd.DataFrame:
|
203
188
|
rows = DB.session.execute(
|
204
189
|
sqlalchemy.select(
|
205
190
|
Activity.id,
|
@@ -226,31 +211,33 @@ def query_activity_meta() -> pd.DataFrame:
|
|
226
211
|
)
|
227
212
|
.join(Activity.equipment)
|
228
213
|
.join(Activity.kind)
|
214
|
+
.where(*clauses)
|
229
215
|
.order_by(Activity.start)
|
230
216
|
).all()
|
231
217
|
df = pd.DataFrame(rows)
|
232
218
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
df.loc[mask,
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
219
|
+
if len(df):
|
220
|
+
for old, new in [
|
221
|
+
("elapsed_time", "average_speed_elapsed_kmh"),
|
222
|
+
("moving_time", "average_speed_moving_kmh"),
|
223
|
+
]:
|
224
|
+
df[new] = pd.NA
|
225
|
+
mask = df[old].dt.total_seconds() > 0
|
226
|
+
df.loc[mask, new] = df.loc[mask, "distance_km"] / (
|
227
|
+
df.loc[mask, old].dt.total_seconds() / 3_600
|
228
|
+
)
|
229
|
+
|
230
|
+
df["date"] = df["start"].dt.date
|
231
|
+
df["year"] = df["start"].dt.year
|
232
|
+
df["month"] = df["start"].dt.month
|
233
|
+
df["day"] = df["start"].dt.day
|
234
|
+
df["week"] = df["start"].dt.isocalendar().week
|
235
|
+
df["day_of_week"] = df["start"].dt.day_of_week
|
236
|
+
df["iso_year"] = df["start"].dt.isocalendar().year
|
237
|
+
df["hours"] = df["elapsed_time"].dt.total_seconds() / 3_600
|
238
|
+
df["hours_moving"] = df["moving_time"].dt.total_seconds() / 3_600
|
239
|
+
|
240
|
+
df.index = df["id"]
|
254
241
|
|
255
242
|
return df
|
256
243
|
|
@@ -276,6 +263,23 @@ class Equipment(DB.Model):
|
|
276
263
|
__table_args__ = (sa.UniqueConstraint("name", name="equipments_name"),)
|
277
264
|
|
278
265
|
|
266
|
+
def get_or_make_equipment(name: str, config: Config) -> Equipment:
|
267
|
+
equipments = DB.session.scalars(
|
268
|
+
sqlalchemy.select(Equipment).where(Equipment.name == name)
|
269
|
+
).all()
|
270
|
+
if equipments:
|
271
|
+
assert (
|
272
|
+
len(equipments) == 1
|
273
|
+
), f"There must be only one equipment with name '{name}'."
|
274
|
+
return equipments[0]
|
275
|
+
else:
|
276
|
+
equipment = Equipment(
|
277
|
+
name=name, offset_km=config.equipment_offsets.get(name, 0)
|
278
|
+
)
|
279
|
+
DB.session.add(equipment)
|
280
|
+
return equipment
|
281
|
+
|
282
|
+
|
279
283
|
class Kind(DB.Model):
|
280
284
|
__tablename__ = "kinds"
|
281
285
|
|
@@ -299,20 +303,6 @@ class Kind(DB.Model):
|
|
299
303
|
__table_args__ = (sa.UniqueConstraint("name", name="kinds_name"),)
|
300
304
|
|
301
305
|
|
302
|
-
class SquarePlannerBookmark(DB.Model):
|
303
|
-
__tablename__ = "square_planner_bookmarks"
|
304
|
-
|
305
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
306
|
-
|
307
|
-
zoom: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
308
|
-
x: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
309
|
-
y: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
310
|
-
size: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
311
|
-
name: Mapped[str] = mapped_column(sa.String, nullable=False)
|
312
|
-
|
313
|
-
__table_args__ = (sa.UniqueConstraint("zoom", "x", "y", "size", name="kinds_name"),)
|
314
|
-
|
315
|
-
|
316
306
|
def get_or_make_kind(name: str, config: Config) -> Kind:
|
317
307
|
kinds = DB.session.scalars(sqlalchemy.select(Kind).where(Kind.name == name)).all()
|
318
308
|
if kinds:
|
@@ -327,21 +317,18 @@ def get_or_make_kind(name: str, config: Config) -> Kind:
|
|
327
317
|
return kind
|
328
318
|
|
329
319
|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
)
|
343
|
-
DB.session.add(equipment)
|
344
|
-
return equipment
|
320
|
+
class SquarePlannerBookmark(DB.Model):
|
321
|
+
__tablename__ = "square_planner_bookmarks"
|
322
|
+
|
323
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
324
|
+
|
325
|
+
zoom: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
326
|
+
x: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
327
|
+
y: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
328
|
+
size: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
329
|
+
name: Mapped[str] = mapped_column(sa.String, nullable=False)
|
330
|
+
|
331
|
+
__table_args__ = (sa.UniqueConstraint("zoom", "x", "y", "size", name="kinds_name"),)
|
345
332
|
|
346
333
|
|
347
334
|
class PlotSpec(DB.Model):
|
@@ -383,3 +370,22 @@ class PlotSpec(DB.Model):
|
|
383
370
|
return json.dumps(
|
384
371
|
{key: getattr(self, key) for key in self.FIELDS if getattr(self, key)}
|
385
372
|
)
|
373
|
+
|
374
|
+
|
375
|
+
class Photo(DB.Model):
|
376
|
+
__tablename__ = "photos"
|
377
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
378
|
+
|
379
|
+
filename: Mapped[str] = mapped_column(sa.String, nullable=False)
|
380
|
+
time: Mapped[datetime.datetime] = mapped_column(sa.DateTime, nullable=False)
|
381
|
+
latitude: Mapped[float] = mapped_column(sa.Float, nullable=False)
|
382
|
+
longitude: Mapped[float] = mapped_column(sa.Float, nullable=False)
|
383
|
+
|
384
|
+
activity_id: Mapped[int] = mapped_column(
|
385
|
+
ForeignKey("activities.id", name="activity_id"), nullable=False
|
386
|
+
)
|
387
|
+
activity: Mapped["Activity"] = relationship(back_populates="photos")
|
388
|
+
|
389
|
+
@property
|
390
|
+
def path(self) -> pathlib.Path:
|
391
|
+
return pathlib.Path(self.filename)
|
@@ -15,6 +15,7 @@ from .datamodel import ActivityMeta
|
|
15
15
|
from .datamodel import DB
|
16
16
|
from .datamodel import get_or_make_equipment
|
17
17
|
from .datamodel import get_or_make_kind
|
18
|
+
from .missing_values import some
|
18
19
|
from .paths import activity_extracted_meta_dir
|
19
20
|
from .paths import activity_extracted_time_series_dir
|
20
21
|
from .paths import time_series_dir
|
@@ -82,9 +83,9 @@ def populate_database_from_extracted(config: Config) -> None:
|
|
82
83
|
distance_km=0,
|
83
84
|
equipment=equipment,
|
84
85
|
kind=kind,
|
85
|
-
calories=extracted_metadata.get("calories", None),
|
86
|
-
elevation_gain=extracted_metadata.get("elevation_gain", None),
|
87
|
-
steps=extracted_metadata.get("steps", None),
|
86
|
+
calories=some(extracted_metadata.get("calories", None)),
|
87
|
+
elevation_gain=some(extracted_metadata.get("elevation_gain", None)),
|
88
|
+
steps=some(extracted_metadata.get("steps", None)),
|
88
89
|
path=extracted_metadata.get("path", None),
|
89
90
|
upstream_id=upstream_id,
|
90
91
|
)
|
@@ -92,7 +93,13 @@ def populate_database_from_extracted(config: Config) -> None:
|
|
92
93
|
update_via_time_series(activity, time_series)
|
93
94
|
|
94
95
|
DB.session.add(activity)
|
95
|
-
|
96
|
+
try:
|
97
|
+
DB.session.commit()
|
98
|
+
except sqlalchemy.exc.StatementError:
|
99
|
+
logger.error(
|
100
|
+
f"Could not insert the following activity into the database: {vars(activity)=}"
|
101
|
+
)
|
102
|
+
raise
|
96
103
|
|
97
104
|
enriched_time_series_path = time_series_dir() / f"{activity.id}.parquet"
|
98
105
|
time_series.to_parquet(enriched_time_series_path)
|
@@ -101,8 +108,10 @@ def populate_database_from_extracted(config: Config) -> None:
|
|
101
108
|
def update_via_time_series(
|
102
109
|
activity: Activity, time_series: pd.DataFrame
|
103
110
|
) -> ActivityMeta:
|
104
|
-
activity.start = time_series["time"].iloc[0]
|
105
|
-
activity.elapsed_time =
|
111
|
+
activity.start = some(time_series["time"].iloc[0])
|
112
|
+
activity.elapsed_time = some(
|
113
|
+
time_series["time"].iloc[-1] - time_series["time"].iloc[0]
|
114
|
+
)
|
106
115
|
activity.distance_km = (
|
107
116
|
time_series["distance_km"].iloc[-1] - time_series["distance_km"].iloc[0]
|
108
117
|
)
|
@@ -2,21 +2,33 @@ import dataclasses
|
|
2
2
|
import datetime
|
3
3
|
import re
|
4
4
|
import urllib.parse
|
5
|
+
from collections.abc import Sequence
|
5
6
|
from typing import Optional
|
6
7
|
|
7
8
|
import dateutil.parser
|
8
9
|
import numpy as np
|
9
10
|
import pandas as pd
|
11
|
+
import sqlalchemy
|
12
|
+
|
13
|
+
from .datamodel import Activity
|
14
|
+
from .datamodel import DB
|
15
|
+
from .datamodel import Equipment
|
16
|
+
from .datamodel import Kind
|
17
|
+
from .datamodel import query_activity_meta
|
18
|
+
from .datamodel import Tag
|
10
19
|
|
11
20
|
|
12
21
|
@dataclasses.dataclass
|
13
22
|
class SearchQuery:
|
14
|
-
equipment: list[
|
15
|
-
kind: list[
|
23
|
+
equipment: list[Equipment] = dataclasses.field(default_factory=list)
|
24
|
+
kind: list[Kind] = dataclasses.field(default_factory=list)
|
25
|
+
tag: list[Tag] = dataclasses.field(default_factory=list)
|
16
26
|
name: Optional[str] = None
|
17
27
|
name_case_sensitive: bool = False
|
18
28
|
start_begin: Optional[datetime.date] = None
|
19
29
|
start_end: Optional[datetime.date] = None
|
30
|
+
distance_km_min: Optional[float] = None
|
31
|
+
distance_km_max: Optional[float] = None
|
20
32
|
|
21
33
|
def __str__(self) -> str:
|
22
34
|
bits = []
|
@@ -25,10 +37,14 @@ class SearchQuery:
|
|
25
37
|
if self.equipment:
|
26
38
|
bits.append(
|
27
39
|
"equipment is "
|
28
|
-
+ (" or ".join(f"“{equipment}”" for equipment in self.equipment))
|
40
|
+
+ (" or ".join(f"“{equipment.name}”" for equipment in self.equipment))
|
29
41
|
)
|
30
42
|
if self.kind:
|
31
|
-
bits.append(
|
43
|
+
bits.append(
|
44
|
+
"kind is " + (" or ".join(f"“{kind.name}”" for kind in self.kind))
|
45
|
+
)
|
46
|
+
if self.tag:
|
47
|
+
bits.append("tag is " + (" or ".join(f"“{tag.tag}”" for tag in self.tag)))
|
32
48
|
if self.start_begin:
|
33
49
|
bits.append(f"after “{self.start_begin.isoformat()}”")
|
34
50
|
if self.start_end:
|
@@ -43,27 +59,38 @@ class SearchQuery:
|
|
43
59
|
or self.name
|
44
60
|
or self.start_begin
|
45
61
|
or self.start_end
|
62
|
+
or self.tag
|
63
|
+
or self.distance_km_min
|
64
|
+
or self.distance_km_max
|
46
65
|
)
|
47
66
|
|
48
67
|
def to_primitives(self) -> dict:
|
49
68
|
return {
|
50
|
-
"equipment": self.equipment,
|
51
|
-
"kind": self.kind,
|
69
|
+
"equipment": [equipment.id for equipment in self.equipment],
|
70
|
+
"kind": [kind.id for kind in self.kind],
|
71
|
+
"tag": [tag.id for tag in self.tag],
|
52
72
|
"name": self.name or "",
|
53
73
|
"name_case_sensitive": self.name_case_sensitive,
|
54
74
|
"start_begin": _format_optional_date(self.start_begin),
|
55
75
|
"start_end": _format_optional_date(self.start_end),
|
76
|
+
"distance_km_min": self.distance_km_min,
|
77
|
+
"distance_km_max": self.distance_km_max,
|
56
78
|
}
|
57
79
|
|
58
80
|
@classmethod
|
59
81
|
def from_primitives(cls, d: dict) -> "SearchQuery":
|
60
82
|
return cls(
|
61
|
-
equipment=
|
62
|
-
|
83
|
+
equipment=[
|
84
|
+
DB.session.get_one(Equipment, id) for id in d.get("equipment", [])
|
85
|
+
],
|
86
|
+
kind=[DB.session.get_one(Kind, id) for id in d.get("kind", [])],
|
87
|
+
tag=[DB.session.get_one(Tag, id) for id in d.get("tag", [])],
|
63
88
|
name=d.get("name", None),
|
64
89
|
name_case_sensitive=d.get("name_case_sensitive", False),
|
65
90
|
start_begin=_parse_date_or_none(d.get("start_begin", None)),
|
66
91
|
start_end=_parse_date_or_none(d.get("start_end", None)),
|
92
|
+
distance_km_min=d.get("distance_km_min", None),
|
93
|
+
distance_km_max=d.get("distance_km_max", None),
|
67
94
|
)
|
68
95
|
|
69
96
|
def to_jinja(self) -> dict:
|
@@ -74,9 +101,11 @@ class SearchQuery:
|
|
74
101
|
def to_url_str(self) -> str:
|
75
102
|
variables = []
|
76
103
|
for equipment in self.equipment:
|
77
|
-
variables.append(("equipment", equipment))
|
104
|
+
variables.append(("equipment", equipment.id))
|
78
105
|
for kind in self.kind:
|
79
|
-
variables.append(("kind", kind))
|
106
|
+
variables.append(("kind", kind.id))
|
107
|
+
for tag in self.tag:
|
108
|
+
variables.append(("tag", tag.id))
|
80
109
|
if self.name:
|
81
110
|
variables.append(("name", self.name))
|
82
111
|
if self.name_case_sensitive:
|
@@ -85,6 +114,10 @@ class SearchQuery:
|
|
85
114
|
variables.append(("start_begin", self.start_begin.isoformat()))
|
86
115
|
if self.start_end:
|
87
116
|
variables.append(("start_end", self.start_end.isoformat()))
|
117
|
+
if self.distance_km_min:
|
118
|
+
variables.append(("distance_km_min", self.distance_km_min))
|
119
|
+
if self.distance_km_max:
|
120
|
+
variables.append(("distance_km_max", self.distance_km_max))
|
88
121
|
|
89
122
|
return "&".join(
|
90
123
|
f"{key}={urllib.parse.quote_plus(value)}" for key, value in variables
|
@@ -94,36 +127,47 @@ class SearchQuery:
|
|
94
127
|
def apply_search_query(
|
95
128
|
activity_meta: pd.DataFrame, search_query: SearchQuery
|
96
129
|
) -> pd.DataFrame:
|
97
|
-
|
130
|
+
|
131
|
+
filter_clauses = []
|
98
132
|
|
99
133
|
if search_query.equipment:
|
100
|
-
|
134
|
+
filter_clauses.append(
|
135
|
+
sqlalchemy.or_(
|
136
|
+
*[
|
137
|
+
Activity.equipment == equipment
|
138
|
+
for equipment in search_query.equipment
|
139
|
+
]
|
140
|
+
)
|
141
|
+
)
|
142
|
+
|
101
143
|
if search_query.kind:
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
activity_name,
|
110
|
-
0 if search_query.name_case_sensitive else re.IGNORECASE,
|
111
|
-
)
|
112
|
-
)
|
113
|
-
for activity_name in activity_meta["name"]
|
114
|
-
],
|
115
|
-
index=activity_meta.index,
|
144
|
+
filter_clauses.append(
|
145
|
+
sqlalchemy.or_(*[Activity.kind == kind for kind in search_query.kind])
|
146
|
+
)
|
147
|
+
|
148
|
+
if search_query.tag:
|
149
|
+
filter_clauses.append(
|
150
|
+
sqlalchemy.or_(*[Activity.tags.contains(tag) for tag in search_query.tag])
|
116
151
|
)
|
117
|
-
|
118
|
-
|
119
|
-
|
152
|
+
|
153
|
+
if search_query.name:
|
154
|
+
filter_clauses.append(
|
155
|
+
Activity.name.contains(search_query.name)
|
156
|
+
if search_query.name_case_sensitive
|
157
|
+
else Activity.name.icontains(search_query.name)
|
120
158
|
)
|
121
|
-
mask &= start_begin <= activity_meta["start"]
|
122
|
-
if search_query.start_end is not None:
|
123
|
-
start_end = datetime.datetime.combine(search_query.start_end, datetime.time.max)
|
124
|
-
mask &= activity_meta["start"] <= start_end
|
125
159
|
|
126
|
-
|
160
|
+
if search_query.start_begin:
|
161
|
+
filter_clauses.append(Activity.start <= search_query.start_begin)
|
162
|
+
if search_query.start_end:
|
163
|
+
filter_clauses.append(Activity.start < search_query.start_end)
|
164
|
+
|
165
|
+
if search_query.distance_km_min:
|
166
|
+
filter_clauses.append(Activity.distance_km >= search_query.distance_km_min)
|
167
|
+
if search_query.distance_km_max:
|
168
|
+
filter_clauses.append(Activity.distance_km <= search_query.distance_km_max)
|
169
|
+
|
170
|
+
return query_activity_meta(filter_clauses)
|
127
171
|
|
128
172
|
|
129
173
|
def _format_optional_date(date: Optional[datetime.date]) -> str:
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from typing import Any
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import numpy as np
|
5
|
+
import pandas as pd
|
6
|
+
|
7
|
+
|
8
|
+
def some(value: Any) -> Optional[Any]:
|
9
|
+
if value is None:
|
10
|
+
return None
|
11
|
+
elif np.isnan(value):
|
12
|
+
return None
|
13
|
+
elif pd.isna(value):
|
14
|
+
return None
|
15
|
+
else:
|
16
|
+
return value
|
@@ -53,6 +53,7 @@ _strava_last_activity_date_path = _cache_dir / "strava-last-activity-date.json"
|
|
53
53
|
_new_config_file = pathlib.Path("config.json")
|
54
54
|
_activity_meta_override_dir = pathlib.Path("Metadata Override")
|
55
55
|
_time_series_dir = pathlib.Path("Time Series")
|
56
|
+
_photos_dir = pathlib.Path("Photos")
|
56
57
|
|
57
58
|
|
58
59
|
cache_dir = dir_wrapper(_cache_dir)
|
@@ -65,6 +66,7 @@ tiles_per_time_series = dir_wrapper(_tiles_per_time_series)
|
|
65
66
|
strava_api_dir = dir_wrapper(_strava_api_dir)
|
66
67
|
activity_meta_override_dir = dir_wrapper(_activity_meta_override_dir)
|
67
68
|
time_series_dir = dir_wrapper(_time_series_dir)
|
69
|
+
PHOTOS_DIR = dir_wrapper(_photos_dir)
|
68
70
|
|
69
71
|
activities_file = file_wrapper(_activities_file)
|
70
72
|
strava_dynamic_config_path = file_wrapper(_strava_dynamic_config_path)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import numpy as np
|
2
|
+
|
3
|
+
from .missing_values import some
|
4
|
+
|
5
|
+
|
6
|
+
def test_none() -> None:
|
7
|
+
assert some(None) == None
|
8
|
+
|
9
|
+
|
10
|
+
def test_nan() -> None:
|
11
|
+
assert some(np.nan) == None
|
12
|
+
|
13
|
+
|
14
|
+
def test_float() -> None:
|
15
|
+
assert some(1.0) == 1.0
|
16
|
+
|
17
|
+
|
18
|
+
def test_integer() -> None:
|
19
|
+
assert some(1) == 1
|
@@ -176,7 +176,7 @@ def _process_activity(
|
|
176
176
|
activity_tiles["time"],
|
177
177
|
zip(activity_tiles["tile_x"], activity_tiles["tile_y"]),
|
178
178
|
):
|
179
|
-
if activity
|
179
|
+
if activity.kind.consider_for_achievements:
|
180
180
|
if tile not in tile_state["tile_visits"][zoom]:
|
181
181
|
new_tile_history_soa["activity_id"].append(activity_id)
|
182
182
|
new_tile_history_soa["time"].append(time)
|