geo-activity-playground 0.40.1__py3-none-any.whl → 0.42.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.
Files changed (38) hide show
  1. geo_activity_playground/alembic/versions/38882503dc7c_add_tags_to_activities.py +70 -0
  2. geo_activity_playground/alembic/versions/script.py.mako +0 -6
  3. geo_activity_playground/core/activities.py +21 -44
  4. geo_activity_playground/core/datamodel.py +121 -60
  5. geo_activity_playground/core/enrichment.py +11 -4
  6. geo_activity_playground/core/missing_values.py +13 -0
  7. geo_activity_playground/core/test_missing_values.py +19 -0
  8. geo_activity_playground/explorer/tile_visits.py +1 -1
  9. geo_activity_playground/webui/app.py +7 -3
  10. geo_activity_playground/webui/blueprints/activity_blueprint.py +38 -13
  11. geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +50 -25
  12. geo_activity_playground/webui/blueprints/calendar_blueprint.py +12 -4
  13. geo_activity_playground/webui/blueprints/eddington_blueprints.py +253 -0
  14. geo_activity_playground/webui/blueprints/entry_views.py +30 -15
  15. geo_activity_playground/webui/blueprints/explorer_blueprint.py +83 -9
  16. geo_activity_playground/webui/blueprints/settings_blueprint.py +32 -0
  17. geo_activity_playground/webui/blueprints/summary_blueprint.py +102 -42
  18. geo_activity_playground/webui/columns.py +37 -0
  19. geo_activity_playground/webui/templates/activity/edit.html.j2 +15 -0
  20. geo_activity_playground/webui/templates/activity/show.html.j2 +27 -5
  21. geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +24 -8
  22. geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +150 -0
  23. geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +150 -0
  24. geo_activity_playground/webui/templates/explorer/server-side.html.j2 +72 -0
  25. geo_activity_playground/webui/templates/home.html.j2 +14 -5
  26. geo_activity_playground/webui/templates/page.html.j2 +10 -1
  27. geo_activity_playground/webui/templates/settings/index.html.j2 +9 -0
  28. geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +17 -0
  29. geo_activity_playground/webui/templates/settings/tags-list.html.j2 +19 -0
  30. geo_activity_playground/webui/templates/settings/tags-new.html.j2 +17 -0
  31. geo_activity_playground/webui/templates/summary/index.html.j2 +91 -2
  32. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/METADATA +2 -1
  33. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/RECORD +37 -27
  34. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/WHEEL +1 -1
  35. geo_activity_playground/webui/blueprints/eddington_blueprint.py +0 -194
  36. /geo_activity_playground/webui/templates/eddington/{index.html.j2 → distance.html.j2} +0 -0
  37. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/LICENSE +0 -0
  38. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,70 @@
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 = "38882503dc7c"
10
+ down_revision: Union[str, None] = "93cc82ad1b60"
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
+ "tags",
19
+ sa.Column("id", sa.Integer(), nullable=False),
20
+ sa.Column("tag", sa.String(), nullable=False),
21
+ sa.PrimaryKeyConstraint("id"),
22
+ sa.UniqueConstraint("tag"),
23
+ sa.UniqueConstraint("tag", name="tags_tag"),
24
+ )
25
+ op.create_table(
26
+ "activity_tag_association_table",
27
+ sa.Column("left_id", sa.Integer(), nullable=False),
28
+ sa.Column("right_id", sa.Integer(), nullable=False),
29
+ sa.ForeignKeyConstraint(
30
+ ["left_id"],
31
+ ["activities.id"],
32
+ ),
33
+ sa.ForeignKeyConstraint(
34
+ ["right_id"],
35
+ ["tags.id"],
36
+ ),
37
+ sa.PrimaryKeyConstraint("left_id", "right_id"),
38
+ )
39
+ with op.batch_alter_table("plot_specs", schema=None) as batch_op:
40
+ batch_op.alter_column("mark", existing_type=sa.VARCHAR(), nullable=False)
41
+ batch_op.alter_column("x", existing_type=sa.VARCHAR(), nullable=False)
42
+ batch_op.alter_column("y", existing_type=sa.VARCHAR(), nullable=False)
43
+ batch_op.alter_column("color", existing_type=sa.VARCHAR(), nullable=False)
44
+ batch_op.alter_column("shape", existing_type=sa.VARCHAR(), nullable=False)
45
+ batch_op.alter_column("size", existing_type=sa.VARCHAR(), nullable=False)
46
+ batch_op.alter_column("row", existing_type=sa.VARCHAR(), nullable=False)
47
+ batch_op.alter_column("opacity", existing_type=sa.VARCHAR(), nullable=False)
48
+ batch_op.alter_column("column", existing_type=sa.VARCHAR(), nullable=False)
49
+ batch_op.alter_column("facet", existing_type=sa.VARCHAR(), nullable=False)
50
+
51
+ # ### end Alembic commands ###
52
+
53
+
54
+ def downgrade() -> None:
55
+ # ### commands auto generated by Alembic - please adjust! ###
56
+ with op.batch_alter_table("plot_specs", schema=None) as batch_op:
57
+ batch_op.alter_column("facet", existing_type=sa.VARCHAR(), nullable=True)
58
+ batch_op.alter_column("column", existing_type=sa.VARCHAR(), nullable=True)
59
+ batch_op.alter_column("opacity", existing_type=sa.VARCHAR(), nullable=True)
60
+ batch_op.alter_column("row", existing_type=sa.VARCHAR(), nullable=True)
61
+ batch_op.alter_column("size", existing_type=sa.VARCHAR(), nullable=True)
62
+ batch_op.alter_column("shape", existing_type=sa.VARCHAR(), nullable=True)
63
+ batch_op.alter_column("color", existing_type=sa.VARCHAR(), nullable=True)
64
+ batch_op.alter_column("y", existing_type=sa.VARCHAR(), nullable=True)
65
+ batch_op.alter_column("x", existing_type=sa.VARCHAR(), nullable=True)
66
+ batch_op.alter_column("mark", existing_type=sa.VARCHAR(), nullable=True)
67
+
68
+ op.drop_table("activity_tag_association_table")
69
+ op.drop_table("tags")
70
+ # ### end Alembic commands ###
@@ -1,10 +1,4 @@
1
- """${message}
2
1
 
3
- Revision ID: ${up_revision}
4
- Revises: ${down_revision | comma,n}
5
- Create Date: ${create_date}
6
-
7
- """
8
2
  from typing import Sequence, Union
9
3
 
10
4
  from alembic import op
@@ -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
 
@@ -16,21 +17,11 @@ from geo_activity_playground.core.datamodel import Activity
16
17
  from geo_activity_playground.core.datamodel import ActivityMeta
17
18
  from geo_activity_playground.core.datamodel import DB
18
19
  from geo_activity_playground.core.datamodel import Kind
20
+ from geo_activity_playground.core.datamodel import query_activity_meta
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
22
24
 
23
- def make_activity_meta() -> ActivityMeta:
24
- return ActivityMeta(
25
- calories=None,
26
- commute=False,
27
- consider_for_achievements=True,
28
- equipment="Unknown",
29
- kind="Unknown",
30
- steps=None,
31
- )
32
-
33
-
34
25
  class ActivityRepository:
35
26
  def __len__(self) -> int:
36
27
  return len(self.get_activity_ids())
@@ -51,14 +42,14 @@ class ActivityRepository:
51
42
  else:
52
43
  return None
53
44
 
54
- def get_activity_ids(self, only_achievements: bool = False) -> list[int]:
45
+ def get_activity_ids(self, only_achievements: bool = False) -> Sequence[int]:
55
46
  query = sqlalchemy.select(Activity.id)
56
47
  if only_achievements:
57
48
  query = query.where(Kind.consider_for_achievements)
58
- result = DB.session.scalars(query).all()
49
+ result = DB.session.scalars(query.order_by(Activity.start)).all()
59
50
  return result
60
51
 
61
- def iter_activities(self, new_to_old=True, drop_na=False) -> list[Activity]:
52
+ def iter_activities(self, new_to_old=True, drop_na=False) -> Sequence[Activity]:
62
53
  query = sqlalchemy.select(Activity)
63
54
  if drop_na:
64
55
  query = query.where(Activity.start.is_not(None))
@@ -79,22 +70,8 @@ class ActivityRepository:
79
70
 
80
71
  @property
81
72
  def meta(self) -> pd.DataFrame:
82
- activities = self.iter_activities(new_to_old=False, drop_na=True)
83
- df = pd.DataFrame([activity.to_dict() for activity in activities])
84
- df["date"] = df["start"].dt.date
85
- df["year"] = [start.year for start in df["start"]]
86
- df["month"] = [start.month for start in df["start"]]
87
- df["day"] = [start.day for start in df["start"]]
88
- df["week"] = [start.isocalendar().week for start in df["start"]]
89
- df["day_of_week"] = df["start"].dt.day_of_week
90
- df["iso_year"] = [start.isocalendar().year for start in df["start"]]
91
- df["hours"] = [
92
- elapsed_time.total_seconds() / 3600 for elapsed_time in df["elapsed_time"]
93
- ]
94
- df["hours_moving"] = [
95
- moving_time.total_seconds() / 3600 for moving_time in df["moving_time"]
96
- ]
97
- df.index = df["id"]
73
+ df = query_activity_meta()
74
+
98
75
  return df
99
76
 
100
77
 
@@ -114,8 +91,8 @@ def inter_quartile_range(values):
114
91
  return np.quantile(values, 0.75) - np.quantile(values, 0.25)
115
92
 
116
93
 
117
- def make_geojson_color_line(time_series: pd.DataFrame) -> str:
118
- low, high, clamp_speed = _make_speed_clamp(time_series["speed"])
94
+ def make_geojson_color_line(time_series: pd.DataFrame, column: str) -> str:
95
+ low, high, clamp_value = _make_value_clamp(time_series[column])
119
96
  cmap = matplotlib.colormaps["viridis"]
120
97
  features = [
121
98
  geojson.Feature(
@@ -126,8 +103,8 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
126
103
  ]
127
104
  ),
128
105
  properties={
129
- "speed": next_row["speed"] if np.isfinite(next_row["speed"]) else 0.0,
130
- "color": matplotlib.colors.to_hex(cmap(clamp_speed(next_row["speed"]))),
106
+ column: (next_row[column] if np.isfinite(next_row[column]) else 0.0),
107
+ "color": matplotlib.colors.to_hex(cmap(clamp_value(next_row[column]))),
131
108
  },
132
109
  )
133
110
  for _, group in time_series.groupby("segment_id")
@@ -137,21 +114,21 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
137
114
  return geojson.dumps(feature_collection)
138
115
 
139
116
 
140
- def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, Any]:
141
- low, high, clamp_speed = _make_speed_clamp(time_series["speed"])
117
+ def make_color_bar(time_series: pd.Series, format: str) -> dict[str, Any]:
118
+ low, high, clamp_value = _make_value_clamp(time_series)
142
119
  cmap = matplotlib.colormaps["viridis"]
143
120
  colors = [
144
- (f"{speed:.1f}", matplotlib.colors.to_hex(cmap(clamp_speed(speed))))
145
- for speed in np.linspace(low, high, 10)
121
+ (f"{value:{format}}", matplotlib.colors.to_hex(cmap(clamp_value(value))))
122
+ for value in np.linspace(low, high, 10)
146
123
  ]
147
124
  return {"low": low, "high": high, "colors": colors}
148
125
 
149
126
 
150
- def _make_speed_clamp(speeds: pd.Series) -> tuple[float, float, Callable]:
151
- speed_without_na = speeds.dropna()
152
- low = min(speed_without_na)
127
+ def _make_value_clamp(values: pd.Series) -> tuple[float, float, Callable]:
128
+ values_without_na = values.dropna()
129
+ low = min(values_without_na)
153
130
  high = min(
154
- max(speed_without_na),
155
- np.median(speed_without_na) + 1.5 * inter_quartile_range(speed_without_na),
131
+ max(values_without_na),
132
+ np.median(values_without_na) + 1.5 * inter_quartile_range(values_without_na),
156
133
  )
157
- return low, high, lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
134
+ return low, high, lambda value: min(max((value - low) / (high - low), 0.0), 1.0)
@@ -10,8 +10,10 @@ import pandas as pd
10
10
  import sqlalchemy
11
11
  import sqlalchemy as sa
12
12
  from flask_sqlalchemy import SQLAlchemy
13
+ from sqlalchemy import Column
13
14
  from sqlalchemy import ForeignKey
14
15
  from sqlalchemy import String
16
+ from sqlalchemy import Table
15
17
  from sqlalchemy.orm import DeclarativeBase
16
18
  from sqlalchemy.orm import Mapped
17
19
  from sqlalchemy.orm import mapped_column
@@ -53,6 +55,13 @@ class Base(DeclarativeBase):
53
55
 
54
56
  DB = SQLAlchemy(model_class=Base)
55
57
 
58
+ activity_tag_association_table = Table(
59
+ "activity_tag_association_table",
60
+ Base.metadata,
61
+ Column("left_id", ForeignKey("activities.id"), primary_key=True),
62
+ Column("right_id", ForeignKey("tags.id"), primary_key=True),
63
+ )
64
+
56
65
 
57
66
  class Activity(DB.Model):
58
67
  __tablename__ = "activities"
@@ -104,8 +113,9 @@ class Activity(DB.Model):
104
113
  )
105
114
  kind: Mapped["Kind"] = relationship(back_populates="activities")
106
115
 
107
- def __getitem__(self, item) -> Any:
108
- return self.to_dict()[item]
116
+ tags: Mapped[list["Tag"]] = relationship(
117
+ secondary=activity_tag_association_table, back_populates="activities"
118
+ )
109
119
 
110
120
  def __str__(self) -> str:
111
121
  return f"{self.start} {self.name}"
@@ -114,11 +124,15 @@ class Activity(DB.Model):
114
124
  def average_speed_moving_kmh(self) -> Optional[float]:
115
125
  if self.moving_time:
116
126
  return self.distance_km / (self.moving_time.total_seconds() / 3_600)
127
+ else:
128
+ return None
117
129
 
118
130
  @property
119
131
  def average_speed_elapsed_kmh(self) -> Optional[float]:
120
132
  if self.elapsed_time:
121
133
  return self.distance_km / (self.elapsed_time.total_seconds() / 3_600)
134
+ else:
135
+ return None
122
136
 
123
137
  @property
124
138
  def raw_time_series(self) -> pd.DataFrame:
@@ -141,38 +155,85 @@ class Activity(DB.Model):
141
155
  else:
142
156
  return self.raw_time_series
143
157
 
144
- def to_dict(self) -> ActivityMeta:
145
- equipment = self.equipment.name if self.equipment is not None else "Unknown"
146
- kind = self.kind.name if self.kind is not None else "Unknown"
147
- consider_for_achievements = (
148
- self.kind.consider_for_achievements if self.kind is not None else True
158
+
159
+ class Tag(DB.Model):
160
+ __tablename__ = "tags"
161
+ __table_args__ = (sa.UniqueConstraint("tag", name="tags_tag"),)
162
+
163
+ id: Mapped[int] = mapped_column(primary_key=True)
164
+ tag: Mapped[str] = mapped_column(String, unique=True)
165
+
166
+ activities: Mapped[list[Activity]] = relationship(
167
+ secondary=activity_tag_association_table, back_populates="tags"
168
+ )
169
+
170
+
171
+ def get_or_make_tag(tag: str) -> Tag:
172
+ tags = DB.session.scalars(sqlalchemy.select(Tag).where(Tag.tag == tag)).all()
173
+ if tags:
174
+ assert len(tags) == 1, f"There must be only one tag with name '{tag}'."
175
+ return tags[0]
176
+ else:
177
+ tag = Tag(tag=tag)
178
+ DB.session.add(tag)
179
+ return tag
180
+
181
+
182
+ def query_activity_meta() -> pd.DataFrame:
183
+ rows = DB.session.execute(
184
+ sqlalchemy.select(
185
+ Activity.id,
186
+ Activity.name,
187
+ Activity.path,
188
+ Activity.distance_km,
189
+ Activity.start,
190
+ Activity.elapsed_time,
191
+ Activity.moving_time,
192
+ Activity.start_latitude,
193
+ Activity.start_longitude,
194
+ Activity.end_latitude,
195
+ Activity.end_longitude,
196
+ Activity.elevation_gain,
197
+ Activity.start_elevation,
198
+ Activity.end_elevation,
199
+ Activity.calories,
200
+ Activity.steps,
201
+ Activity.num_new_tiles_14,
202
+ Activity.num_new_tiles_17,
203
+ Kind.consider_for_achievements,
204
+ Equipment.name.label("equipment"),
205
+ Kind.name.label("kind"),
149
206
  )
150
- return ActivityMeta(
151
- id=self.id,
152
- name=self.name,
153
- path=self.path,
154
- distance_km=self.distance_km,
155
- start=self.start,
156
- elapsed_time=self.elapsed_time,
157
- moving_time=self.moving_time,
158
- start_latitude=self.start_latitude,
159
- start_longitude=self.start_longitude,
160
- end_latitude=self.end_latitude,
161
- end_longitude=self.end_longitude,
162
- elevation_gain=self.elevation_gain,
163
- start_elevation=self.start_elevation,
164
- end_elevation=self.end_elevation,
165
- calories=self.calories,
166
- steps=self.steps,
167
- num_new_tiles_14=self.num_new_tiles_14,
168
- num_new_tiles_17=self.num_new_tiles_17,
169
- equipment=equipment,
170
- kind=kind,
171
- average_speed_moving_kmh=self.average_speed_moving_kmh,
172
- average_speed_elapsed_kmh=self.average_speed_elapsed_kmh,
173
- consider_for_achievements=consider_for_achievements,
207
+ .join(Activity.equipment)
208
+ .join(Activity.kind)
209
+ .order_by(Activity.start)
210
+ ).all()
211
+ df = pd.DataFrame(rows)
212
+
213
+ for old, new in [
214
+ ("elapsed_time", "average_speed_elapsed_kmh"),
215
+ ("moving_time", "average_speed_moving_kmh"),
216
+ ]:
217
+ df[new] = pd.NA
218
+ mask = df[old].dt.total_seconds() > 0
219
+ df.loc[mask, new] = df.loc[mask, "distance_km"] / (
220
+ df.loc[mask, old].dt.total_seconds() / 3_600
174
221
  )
175
222
 
223
+ df["date"] = df["start"].dt.date
224
+ df["year"] = df["start"].dt.year
225
+ df["month"] = df["start"].dt.month
226
+ df["day"] = df["start"].dt.day
227
+ df["week"] = df["start"].dt.isocalendar().week
228
+ df["day_of_week"] = df["start"].dt.day_of_week
229
+ df["iso_year"] = df["start"].dt.isocalendar().year
230
+ df["hours"] = df["elapsed_time"].dt.total_seconds() / 3_600
231
+ df["hours_moving"] = df["moving_time"].dt.total_seconds() / 3_600
232
+
233
+ df.index = df["id"]
234
+
235
+ return df
236
+
176
237
 
177
238
  class Equipment(DB.Model):
178
239
  __tablename__ = "equipments"
@@ -195,6 +256,23 @@ class Equipment(DB.Model):
195
256
  __table_args__ = (sa.UniqueConstraint("name", name="equipments_name"),)
196
257
 
197
258
 
259
+ def get_or_make_equipment(name: str, config: Config) -> Equipment:
260
+ equipments = DB.session.scalars(
261
+ sqlalchemy.select(Equipment).where(Equipment.name == name)
262
+ ).all()
263
+ if equipments:
264
+ assert (
265
+ len(equipments) == 1
266
+ ), f"There must be only one equipment with name '{name}'."
267
+ return equipments[0]
268
+ else:
269
+ equipment = Equipment(
270
+ name=name, offset_km=config.equipment_offsets.get(name, 0)
271
+ )
272
+ DB.session.add(equipment)
273
+ return equipment
274
+
275
+
198
276
  class Kind(DB.Model):
199
277
  __tablename__ = "kinds"
200
278
 
@@ -218,20 +296,6 @@ class Kind(DB.Model):
218
296
  __table_args__ = (sa.UniqueConstraint("name", name="kinds_name"),)
219
297
 
220
298
 
221
- class SquarePlannerBookmark(DB.Model):
222
- __tablename__ = "square_planner_bookmarks"
223
-
224
- id: Mapped[int] = mapped_column(primary_key=True)
225
-
226
- zoom: Mapped[int] = mapped_column(sa.Integer, nullable=False)
227
- x: Mapped[int] = mapped_column(sa.Integer, nullable=False)
228
- y: Mapped[int] = mapped_column(sa.Integer, nullable=False)
229
- size: Mapped[int] = mapped_column(sa.Integer, nullable=False)
230
- name: Mapped[str] = mapped_column(sa.String, nullable=False)
231
-
232
- __table_args__ = (sa.UniqueConstraint("zoom", "x", "y", "size", name="kinds_name"),)
233
-
234
-
235
299
  def get_or_make_kind(name: str, config: Config) -> Kind:
236
300
  kinds = DB.session.scalars(sqlalchemy.select(Kind).where(Kind.name == name)).all()
237
301
  if kinds:
@@ -246,21 +310,18 @@ def get_or_make_kind(name: str, config: Config) -> Kind:
246
310
  return kind
247
311
 
248
312
 
249
- def get_or_make_equipment(name: str, config: Config) -> Equipment:
250
- equipments = DB.session.scalars(
251
- sqlalchemy.select(Equipment).where(Equipment.name == name)
252
- ).all()
253
- if equipments:
254
- assert (
255
- len(equipments) == 1
256
- ), f"There must be only one equipment with name '{name}'."
257
- return equipments[0]
258
- else:
259
- equipment = Equipment(
260
- name=name, offset_km=config.equipment_offsets.get(name, 0)
261
- )
262
- DB.session.add(equipment)
263
- return equipment
313
+ class SquarePlannerBookmark(DB.Model):
314
+ __tablename__ = "square_planner_bookmarks"
315
+
316
+ id: Mapped[int] = mapped_column(primary_key=True)
317
+
318
+ zoom: Mapped[int] = mapped_column(sa.Integer, nullable=False)
319
+ x: Mapped[int] = mapped_column(sa.Integer, nullable=False)
320
+ y: Mapped[int] = mapped_column(sa.Integer, nullable=False)
321
+ size: Mapped[int] = mapped_column(sa.Integer, nullable=False)
322
+ name: Mapped[str] = mapped_column(sa.String, nullable=False)
323
+
324
+ __table_args__ = (sa.UniqueConstraint("zoom", "x", "y", "size", name="kinds_name"),)
264
325
 
265
326
 
266
327
  class PlotSpec(DB.Model):
@@ -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
- DB.session.commit()
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)
@@ -0,0 +1,13 @@
1
+ from typing import Optional
2
+ from typing import Union
3
+
4
+ import numpy as np
5
+
6
+
7
+ def some(value) -> Optional[Union[float, int]]:
8
+ if value is None:
9
+ return None
10
+ elif np.isnan(value):
11
+ return None
12
+ else:
13
+ return value
@@ -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["consider_for_achievements"]:
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)
@@ -28,7 +28,7 @@ from .blueprints.activity_blueprint import make_activity_blueprint
28
28
  from .blueprints.auth_blueprint import make_auth_blueprint
29
29
  from .blueprints.bubble_chart_blueprint import make_bubble_chart_blueprint
30
30
  from .blueprints.calendar_blueprint import make_calendar_blueprint
31
- from .blueprints.eddington_blueprint import register_eddington_blueprint
31
+ from .blueprints.eddington_blueprints import register_eddington_blueprint
32
32
  from .blueprints.entry_views import register_entry_views
33
33
  from .blueprints.equipment_blueprint import make_equipment_blueprint
34
34
  from .blueprints.explorer_blueprint import make_explorer_blueprint
@@ -70,7 +70,7 @@ def web_ui_main(
70
70
 
71
71
  app = Flask(__name__)
72
72
 
73
- database_path = basedir / "database.sqlite"
73
+ database_path = pathlib.Path("database.sqlite")
74
74
  logger.info(f"Using database file at '{database_path.absolute()}'.")
75
75
  app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{database_path.absolute()}"
76
76
  app.config["ALEMBIC"] = {"script_location": "../alembic/versions"}
@@ -137,7 +137,11 @@ def web_ui_main(
137
137
  "/eddington": register_eddington_blueprint(repository, search_query_history),
138
138
  "/equipment": make_equipment_blueprint(repository, config),
139
139
  "/explorer": make_explorer_blueprint(
140
- authenticator, repository, tile_visit_accessor, config_accessor
140
+ authenticator,
141
+ tile_visit_accessor,
142
+ config_accessor,
143
+ tile_getter,
144
+ image_transforms,
141
145
  ),
142
146
  "/heatmap": make_heatmap_blueprint(
143
147
  repository, tile_visit_accessor, config_accessor(), search_query_history