geo-activity-playground 0.21.1__tar.gz → 0.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/PKG-INFO +1 -1
  2. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/__main__.py +7 -27
  3. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/activities.py +1 -1
  4. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/heatmap.py +2 -2
  5. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/explorer/grid_file.py +7 -5
  6. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/explorer/tile_visits.py +46 -31
  7. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/explorer/video.py +3 -2
  8. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/importers/directory.py +4 -4
  9. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/importers/strava_api.py +6 -6
  10. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/app.py +44 -10
  11. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/calendar_controller.py +0 -3
  12. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/eddington_controller.py +0 -3
  13. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/entry_controller.py +0 -2
  14. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/equipment_controller.py +0 -3
  15. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/explorer_controller.py +10 -16
  16. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/heatmap_controller.py +15 -20
  17. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/search_controller.py +0 -2
  18. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/square_planner_controller.py +6 -7
  19. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/page.html.j2 +3 -0
  20. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/search.html.j2 +1 -1
  21. geo_activity_playground-0.22.0/geo_activity_playground/webui/templates/upload.html.j2 +36 -0
  22. geo_activity_playground-0.22.0/geo_activity_playground/webui/upload_controller.py +106 -0
  23. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/pyproject.toml +10 -4
  24. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/LICENSE +0 -0
  25. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/__init__.py +0 -0
  26. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/__init__.py +0 -0
  27. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/activity_parsers.py +0 -0
  28. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/cache_migrations.py +0 -0
  29. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/config.py +0 -0
  30. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/coordinates.py +0 -0
  31. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/paths.py +0 -0
  32. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/similarity.py +0 -0
  33. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/tasks.py +0 -0
  34. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/test_tiles.py +0 -0
  35. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/core/tiles.py +0 -0
  36. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/explorer/__init__.py +0 -0
  37. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/importers/strava_checkout.py +0 -0
  38. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/importers/test_directory.py +0 -0
  39. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
  40. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/activity_controller.py +0 -0
  41. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/config_controller.py +0 -0
  42. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/locations_controller.py +0 -0
  43. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  44. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  45. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  46. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  47. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  48. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  49. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  50. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/favicon.ico +0 -0
  51. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  52. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
  53. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  54. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/strava_controller.py +0 -0
  55. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/summary_controller.py +0 -0
  56. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/activity-day.html.j2 +0 -0
  57. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/activity-lines.html.j2 +0 -0
  58. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/activity-name.html.j2 +0 -0
  59. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/activity.html.j2 +0 -0
  60. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/calendar-month.html.j2 +0 -0
  61. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/calendar.html.j2 +0 -0
  62. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/config.html.j2 +0 -0
  63. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/eddington.html.j2 +0 -0
  64. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/equipment.html.j2 +0 -0
  65. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/explorer.html.j2 +0 -0
  66. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/heatmap.html.j2 +0 -0
  67. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/index.html.j2 +0 -0
  68. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/locations.html.j2 +0 -0
  69. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/square-planner.html.j2 +0 -0
  70. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/strava-connect.html.j2 +0 -0
  71. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/templates/summary.html.j2 +0 -0
  72. {geo_activity_playground-0.21.1 → geo_activity_playground-0.22.0}/geo_activity_playground/webui/tile_controller.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.21.1
3
+ Version: 0.22.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -7,17 +7,13 @@ import sys
7
7
  import coloredlogs
8
8
 
9
9
  from .importers.strava_checkout import convert_strava_checkout
10
- from .importers.strava_checkout import import_from_strava_checkout
11
10
  from geo_activity_playground.core.activities import ActivityRepository
12
- from geo_activity_playground.core.activities import embellish_time_series
13
11
  from geo_activity_playground.core.cache_migrations import apply_cache_migrations
14
12
  from geo_activity_playground.core.config import get_config
15
- from geo_activity_playground.explorer.tile_visits import compute_tile_evolution
16
- from geo_activity_playground.explorer.tile_visits import compute_tile_visits
13
+ from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
17
14
  from geo_activity_playground.explorer.video import explorer_video_main
18
- from geo_activity_playground.importers.directory import import_from_directory
19
- from geo_activity_playground.importers.strava_api import import_from_strava_api
20
15
  from geo_activity_playground.webui.app import webui_main
16
+ from geo_activity_playground.webui.upload_controller import scan_for_activities
21
17
 
22
18
  logger = logging.getLogger(__name__)
23
19
 
@@ -68,7 +64,7 @@ def main() -> None:
68
64
  subparser = subparsers.add_parser("serve", help="Launch webserver")
69
65
  subparser.set_defaults(
70
66
  func=lambda options: webui_main(
71
- make_activity_repository(options.basedir, options.skip_strava),
67
+ *make_activity_repository(options.basedir, options.skip_strava),
72
68
  host=options.host,
73
69
  port=options.port,
74
70
  )
@@ -99,7 +95,7 @@ def main() -> None:
99
95
 
100
96
  def make_activity_repository(
101
97
  basedir: pathlib.Path, skip_strava: bool
102
- ) -> ActivityRepository:
98
+ ) -> tuple[ActivityRepository, TileVisitAccessor, dict]:
103
99
  os.chdir(basedir)
104
100
  apply_cache_migrations()
105
101
  config = get_config()
@@ -111,27 +107,11 @@ def make_activity_repository(
111
107
  sys.exit(1)
112
108
 
113
109
  repository = ActivityRepository()
110
+ tile_visit_accessor = TileVisitAccessor()
114
111
 
115
- if pathlib.Path("Activities").exists():
116
- import_from_directory(
117
- repository,
118
- config.get("metadata_extraction_regexes", []),
119
- )
120
- if pathlib.Path("Strava Export").exists():
121
- import_from_strava_checkout(repository)
122
- if "strava" in config and not skip_strava:
123
- import_from_strava_api(repository)
124
-
125
- if len(repository) == 0:
126
- logger.error(
127
- f"No activities found. You need to either add activity files (GPX, FIT, …) to {basedir/'Activities'} or set up the Strava API. Starting without any activities is unfortunately not supported."
128
- )
129
- sys.exit(1)
112
+ scan_for_activities(repository, tile_visit_accessor, config, skip_strava)
130
113
 
131
- embellish_time_series(repository)
132
- compute_tile_visits(repository)
133
- compute_tile_evolution()
134
- return repository
114
+ return repository, tile_visit_accessor, config
135
115
 
136
116
 
137
117
  if __name__ == "__main__":
@@ -89,7 +89,7 @@ class ActivityRepository:
89
89
  self.meta = pd.concat([old_df, new_df])
90
90
  assert pd.api.types.is_dtype_equal(
91
91
  self.meta["start"].dtype, "datetime64[ns, UTC]"
92
- ), self.meta["start"].dtype
92
+ ), (self.meta["start"].dtype, self.meta["start"].iloc[0])
93
93
  self.save()
94
94
  self._loose_activities = []
95
95
 
@@ -22,7 +22,7 @@ class GeoBounds:
22
22
  lon_max: float
23
23
 
24
24
 
25
- def get_bounds(lat_lon_data: np.array) -> GeoBounds:
25
+ def get_bounds(lat_lon_data: np.ndarray) -> GeoBounds:
26
26
  return GeoBounds(*np.min(lat_lon_data, axis=0), *np.max(lat_lon_data, axis=0))
27
27
 
28
28
 
@@ -106,7 +106,7 @@ def get_sensible_zoom_level(
106
106
  )
107
107
 
108
108
 
109
- def build_map_from_tiles(tile_bounds: TileBounds) -> np.array:
109
+ def build_map_from_tiles(tile_bounds: TileBounds) -> np.ndarray:
110
110
  background = np.zeros((*tile_bounds.shape, 3))
111
111
 
112
112
  for x in range(tile_bounds.x_tile_min, tile_bounds.x_tile_max):
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
16
16
 
17
17
  def get_border_tiles(
18
18
  tiles: pd.DataFrame, zoom: int, tile_bounds: Bounds
19
- ) -> list[list[list[float]]]:
19
+ ) -> list[list[tuple[float, float]]]:
20
20
  logger.info("Generate border tiles …")
21
21
  tile_set = set(zip(tiles["tile_x"], tiles["tile_y"]))
22
22
  border_tiles = set()
@@ -28,7 +28,9 @@ def get_border_tiles(
28
28
  return make_grid_points(border_tiles, zoom)
29
29
 
30
30
 
31
- def get_explored_tiles(tiles: pd.DataFrame, zoom: int) -> list[list[list[float]]]:
31
+ def get_explored_tiles(
32
+ tiles: pd.DataFrame, zoom: int
33
+ ) -> list[list[tuple[float, float]]]:
32
34
  return make_grid_points(zip(tiles["tile_x"], tiles["tile_y"]), zoom)
33
35
 
34
36
 
@@ -66,7 +68,7 @@ def make_explorer_rectangle(
66
68
 
67
69
  def make_grid_points(
68
70
  tiles: Iterable[tuple[int, int]], zoom: int
69
- ) -> list[list[list[float]]]:
71
+ ) -> list[list[tuple[float, float]]]:
70
72
  result = []
71
73
  for tile_x, tile_y in tiles:
72
74
  tile = [
@@ -80,7 +82,7 @@ def make_grid_points(
80
82
  return result
81
83
 
82
84
 
83
- def make_grid_file_gpx(grid_points: list[list[list[float]]]) -> str:
85
+ def make_grid_file_gpx(grid_points: list[list[tuple[float, float]]]) -> str:
84
86
  gpx = gpxpy.gpx.GPX()
85
87
  gpx_track = gpxpy.gpx.GPXTrack()
86
88
  gpx.tracks.append(gpx_track)
@@ -93,7 +95,7 @@ def make_grid_file_gpx(grid_points: list[list[list[float]]]) -> str:
93
95
  return gpx.to_xml()
94
96
 
95
97
 
96
- def make_grid_file_geojson(grid_points: list[list[list[float]]]) -> str:
98
+ def make_grid_file_geojson(grid_points: list[list[tuple[float, float]]]) -> str:
97
99
  fc = geojson.FeatureCollection(
98
100
  [
99
101
  geojson.Feature(
@@ -20,18 +20,39 @@ from geo_activity_playground.core.tiles import interpolate_missing_tile
20
20
 
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
- TILE_EVOLUTION_STATES_PATH = pathlib.Path("Cache/tile-evolution-state.pickle")
24
- TILE_HISTORIES_PATH = pathlib.Path(f"Cache/tile-history.pickle")
25
- TILE_VISITS_PATH = pathlib.Path(f"Cache/tile-visits.pickle")
26
23
 
24
+ class TileVisitAccessor:
25
+ TILE_EVOLUTION_STATES_PATH = pathlib.Path("Cache/tile-evolution-state.pickle")
26
+ TILE_HISTORIES_PATH = pathlib.Path(f"Cache/tile-history.pickle")
27
+ TILE_VISITS_PATH = pathlib.Path(f"Cache/tile-visits.pickle")
27
28
 
28
- def compute_tile_visits(repository: ActivityRepository) -> None:
29
- tile_visits: dict[int, dict[tuple[int, int], dict[str, Any]]] = try_load_pickle(
30
- TILE_VISITS_PATH
31
- ) or collections.defaultdict(dict)
32
- tile_history: dict[int, pd.DataFrame] = try_load_pickle(
33
- TILE_HISTORIES_PATH
34
- ) or collections.defaultdict(pd.DataFrame)
29
+ def __init__(self) -> None:
30
+ self.visits: dict[int, dict[tuple[int, int], dict[str, Any]]] = try_load_pickle(
31
+ self.TILE_VISITS_PATH
32
+ ) or collections.defaultdict(dict)
33
+
34
+ self.histories: dict[int, pd.DataFrame] = try_load_pickle(
35
+ self.TILE_HISTORIES_PATH
36
+ ) or collections.defaultdict(pd.DataFrame)
37
+
38
+ self.states = try_load_pickle(
39
+ self.TILE_EVOLUTION_STATES_PATH
40
+ ) or collections.defaultdict(TileEvolutionState)
41
+
42
+ def save(self) -> None:
43
+ with open(self.TILE_VISITS_PATH, "wb") as f:
44
+ pickle.dump(self.visits, f)
45
+
46
+ with open(self.TILE_HISTORIES_PATH, "wb") as f:
47
+ pickle.dump(self.histories, f)
48
+
49
+ with open(self.TILE_EVOLUTION_STATES_PATH, "wb") as f:
50
+ pickle.dump(self.states, f)
51
+
52
+
53
+ def compute_tile_visits(
54
+ repository: ActivityRepository, tile_visits_accessor: TileVisitAccessor
55
+ ) -> None:
35
56
 
36
57
  work_tracker = WorkTracker("tile-visits")
37
58
  activity_ids_to_process = work_tracker.filter(repository.activity_ids)
@@ -43,8 +64,8 @@ def compute_tile_visits(repository: ActivityRepository) -> None:
43
64
  for zoom in range(20):
44
65
  for time, tile_x, tile_y in _tiles_from_points(time_series, zoom):
45
66
  tile = (tile_x, tile_y)
46
- if tile in tile_visits[zoom]:
47
- d = tile_visits[zoom][tile]
67
+ if tile in tile_visits_accessor.visits[zoom]:
68
+ d = tile_visits_accessor.visits[zoom][tile]
48
69
  if d["first_time"] > time:
49
70
  d["first_time"] = time
50
71
  d["first_id"] = activity_id
@@ -53,7 +74,7 @@ def compute_tile_visits(repository: ActivityRepository) -> None:
53
74
  d["last_id"] = activity_id
54
75
  d["activity_ids"].add(activity_id)
55
76
  else:
56
- tile_visits[zoom][tile] = {
77
+ tile_visits_accessor.visits[zoom][tile] = {
57
78
  "first_time": time,
58
79
  "first_id": activity_id,
59
80
  "last_time": time,
@@ -71,16 +92,14 @@ def compute_tile_visits(repository: ActivityRepository) -> None:
71
92
  work_tracker.mark_done(activity_id)
72
93
 
73
94
  if activity_ids_to_process:
74
- with open(TILE_VISITS_PATH, "wb") as f:
75
- pickle.dump(tile_visits, f)
76
-
77
95
  for zoom, new_rows in new_tile_history_rows.items():
78
96
  new_df = pd.DataFrame(new_rows)
79
97
  new_df.sort_values("time", inplace=True)
80
- tile_history[zoom] = pd.concat([tile_history[zoom], new_df])
98
+ tile_visits_accessor.histories[zoom] = pd.concat(
99
+ [tile_visits_accessor.histories[zoom], new_df]
100
+ )
81
101
 
82
- with open(TILE_HISTORIES_PATH, "wb") as f:
83
- pickle.dump(tile_history, f)
102
+ tile_visits_accessor.save()
84
103
 
85
104
  work_tracker.close()
86
105
 
@@ -123,23 +142,19 @@ class TileEvolutionState:
123
142
  self.square_y: Optional[int] = None
124
143
 
125
144
 
126
- def compute_tile_evolution() -> None:
127
- with open(TILE_HISTORIES_PATH, "rb") as f:
128
- tile_histories = pickle.load(f)
129
-
130
- states = try_load_pickle(TILE_EVOLUTION_STATES_PATH) or collections.defaultdict(
131
- TileEvolutionState
132
- )
133
-
145
+ def compute_tile_evolution(tile_visits_accessor: TileVisitAccessor) -> None:
134
146
  zoom_levels = list(reversed(list(range(20))))
135
147
 
136
148
  for zoom in tqdm(zoom_levels, desc="Compute explorer cluster evolution"):
137
- _compute_cluster_evolution(tile_histories[zoom], states[zoom])
149
+ _compute_cluster_evolution(
150
+ tile_visits_accessor.histories[zoom], tile_visits_accessor.states[zoom]
151
+ )
138
152
  for zoom in tqdm(zoom_levels, desc="Compute explorer square evolution"):
139
- _compute_square_history(tile_histories[zoom], states[zoom])
153
+ _compute_square_history(
154
+ tile_visits_accessor.histories[zoom], tile_visits_accessor.states[zoom]
155
+ )
140
156
 
141
- with open(TILE_EVOLUTION_STATES_PATH, "wb") as f:
142
- pickle.dump(states, f)
157
+ tile_visits_accessor.save()
143
158
 
144
159
 
145
160
  def _compute_cluster_evolution(tiles: pd.DataFrame, s: TileEvolutionState) -> None:
@@ -3,6 +3,7 @@ import math
3
3
  import pathlib
4
4
  from typing import Generator
5
5
  from typing import List
6
+ from typing import Optional
6
7
  from typing import Set
7
8
  from typing import Tuple
8
9
 
@@ -23,8 +24,8 @@ def build_image(
23
24
  brightness: float = 1.0,
24
25
  width: int = 1920,
25
26
  height: int = 1080,
26
- frame_counter: int = None,
27
- ) -> Image.Image:
27
+ frame_counter: int = 0,
28
+ ) -> Optional[Image.Image]:
28
29
  path = pathlib.Path(f"video/{frame_counter:06d}.png")
29
30
  if path.exists():
30
31
  return None
@@ -49,9 +49,9 @@ def import_from_directory(
49
49
  paths_with_errors = [error for error in paths_with_errors if error]
50
50
 
51
51
  for path in tqdm(new_activity_paths, desc="Collate activity metadata"):
52
- activity_id = _get_file_hash(path)
52
+ activity_id = get_file_hash(path)
53
53
  file_metadata_path = file_metadata_dir / f"{activity_id}.pickle"
54
- work_tracker.mark_done(activity_id)
54
+ work_tracker.mark_done(path)
55
55
 
56
56
  if not file_metadata_path.exists():
57
57
  continue
@@ -87,7 +87,7 @@ def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]
87
87
  activity_stream_dir = pathlib.Path("Cache/Activity Timeseries")
88
88
  file_metadata_dir = pathlib.Path("Cache/Activity Metadata")
89
89
 
90
- activity_id = _get_file_hash(path)
90
+ activity_id = get_file_hash(path)
91
91
  timeseries_path = activity_stream_dir / f"{activity_id}.parquet"
92
92
  file_metadata_path = file_metadata_dir / f"{activity_id}.pickle"
93
93
 
@@ -110,7 +110,7 @@ def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]
110
110
  pickle.dump(activity_meta_from_file, f)
111
111
 
112
112
 
113
- def _get_file_hash(path: pathlib.Path) -> int:
113
+ def get_file_hash(path: pathlib.Path) -> int:
114
114
  file_hash = hashlib.blake2s()
115
115
  with open(path, "rb") as f:
116
116
  while chunk := f.read(8192):
@@ -103,12 +103,12 @@ def import_from_strava_api(repository: ActivityRepository) -> None:
103
103
  time.sleep(seconds_to_wait)
104
104
 
105
105
 
106
- def try_import_strava(repository: ActivityRepository) -> None:
107
- get_after = (
108
- repository.last_activity_date().isoformat().replace("+00:00", "Z")
109
- if repository.last_activity_date() is not None
110
- else "2000-01-01T00:00:00Z"
111
- )
106
+ def try_import_strava(repository: ActivityRepository) -> bool:
107
+ last = repository.last_activity_date()
108
+ if last is None:
109
+ get_after = "2000-01-01T00:00:00Z"
110
+ else:
111
+ get_after = last.isoformat().replace("+00:00", "Z")
112
112
 
113
113
  gear_names = {None: "None"}
114
114
 
@@ -9,6 +9,7 @@ from flask import Response
9
9
  from .locations_controller import LocationsController
10
10
  from .search_controller import SearchController
11
11
  from geo_activity_playground.core.activities import ActivityRepository
12
+ from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
12
13
  from geo_activity_playground.webui.activity_controller import ActivityController
13
14
  from geo_activity_playground.webui.calendar_controller import CalendarController
14
15
  from geo_activity_playground.webui.config_controller import ConfigController
@@ -26,6 +27,7 @@ from geo_activity_playground.webui.summary_controller import SummaryController
26
27
  from geo_activity_playground.webui.tile_controller import (
27
28
  TileController,
28
29
  )
30
+ from geo_activity_playground.webui.upload_controller import UploadController
29
31
 
30
32
 
31
33
  def route_activity(app: Flask, repository: ActivityRepository) -> None:
@@ -113,8 +115,10 @@ def route_equipment(app: Flask, repository: ActivityRepository) -> None:
113
115
  return render_template("equipment.html.j2", **equipment_controller.render())
114
116
 
115
117
 
116
- def route_explorer(app: Flask, repository: ActivityRepository) -> None:
117
- explorer_controller = ExplorerController(repository)
118
+ def route_explorer(
119
+ app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
120
+ ) -> None:
121
+ explorer_controller = ExplorerController(repository, tile_visit_accessor)
118
122
 
119
123
  @app.route("/explorer/<zoom>")
120
124
  def explorer(zoom: str):
@@ -159,8 +163,10 @@ def route_explorer(app: Flask, repository: ActivityRepository) -> None:
159
163
  )
160
164
 
161
165
 
162
- def route_heatmap(app: Flask, repository: ActivityRepository) -> None:
163
- heatmap_controller = HeatmapController(repository)
166
+ def route_heatmap(
167
+ app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
168
+ ) -> None:
169
+ heatmap_controller = HeatmapController(repository, tile_visit_accessor)
164
170
 
165
171
  @app.route("/heatmap")
166
172
  def heatmap():
@@ -207,8 +213,10 @@ def route_search(app: Flask, repository: ActivityRepository) -> None:
207
213
  )
208
214
 
209
215
 
210
- def route_square_planner(app: Flask, repository: ActivityRepository) -> None:
211
- controller = SquarePlannerController(repository)
216
+ def route_square_planner(
217
+ app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
218
+ ) -> None:
219
+ controller = SquarePlannerController(repository, tile_visit_accessor)
212
220
 
213
221
  @app.route("/square-planner/<zoom>/<x>/<y>/<size>")
214
222
  def square_planner_planner(zoom, x, y, size):
@@ -303,7 +311,30 @@ def route_tiles(app: Flask, repository: ActivityRepository) -> None:
303
311
  )
304
312
 
305
313
 
306
- def webui_main(repository: ActivityRepository, host: str, port: int) -> None:
314
+ def route_upload(
315
+ app: Flask,
316
+ repository: ActivityRepository,
317
+ tile_visit_accessor: TileVisitAccessor,
318
+ config: dict,
319
+ ):
320
+ upload_controller = UploadController(repository, tile_visit_accessor, config)
321
+
322
+ @app.route("/upload")
323
+ def form():
324
+ return render_template("upload.html.j2", **upload_controller.render_form())
325
+
326
+ @app.route("/upload/receive", methods=["POST"])
327
+ def receive():
328
+ return upload_controller.receive()
329
+
330
+
331
+ def webui_main(
332
+ repository: ActivityRepository,
333
+ tile_visit_accessor: TileVisitAccessor,
334
+ config: dict,
335
+ host: str,
336
+ port: int,
337
+ ) -> None:
307
338
  app = Flask(__name__)
308
339
 
309
340
  route_activity(app, repository)
@@ -311,14 +342,17 @@ def webui_main(repository: ActivityRepository, host: str, port: int) -> None:
311
342
  route_config(app, repository)
312
343
  route_eddington(app, repository)
313
344
  route_equipment(app, repository)
314
- route_explorer(app, repository)
315
- route_heatmap(app, repository)
345
+ route_explorer(app, repository, tile_visit_accessor)
346
+ route_heatmap(app, repository, tile_visit_accessor)
316
347
  route_locations(app, repository)
317
348
  route_search(app, repository)
318
- route_square_planner(app, repository)
349
+ route_square_planner(app, repository, tile_visit_accessor)
319
350
  route_start(app, repository)
320
351
  route_strava(app, host, port)
321
352
  route_summary(app, repository)
322
353
  route_tiles(app, repository)
354
+ route_upload(app, repository, tile_visit_accessor, config)
355
+
356
+ app.config["UPLOAD_FOLDER"] = "Activities"
323
357
 
324
358
  app.run(host=host, port=port)
@@ -1,6 +1,5 @@
1
1
  import collections
2
2
  import datetime
3
- import functools
4
3
 
5
4
  from geo_activity_playground.core.activities import ActivityRepository
6
5
 
@@ -9,7 +8,6 @@ class CalendarController:
9
8
  def __init__(self, repository: ActivityRepository) -> None:
10
9
  self._repository = repository
11
10
 
12
- @functools.cache
13
11
  def render_overview(self) -> dict:
14
12
  meta = self._repository.meta.copy()
15
13
  meta["date"] = meta["start"].dt.date
@@ -41,7 +39,6 @@ class CalendarController:
41
39
  "yearly_distances": yearly_distances,
42
40
  }
43
41
 
44
- @functools.cache
45
42
  def render_month(self, year: int, month: int) -> dict:
46
43
  meta = self._repository.meta.copy()
47
44
  meta["date"] = meta["start"].dt.date
@@ -1,5 +1,3 @@
1
- import functools
2
-
3
1
  import altair as alt
4
2
  import numpy as np
5
3
  import pandas as pd
@@ -11,7 +9,6 @@ class EddingtonController:
11
9
  def __init__(self, repository: ActivityRepository) -> None:
12
10
  self._repository = repository
13
11
 
14
- @functools.cache
15
12
  def render(self) -> dict:
16
13
  activities = self._repository.meta.copy()
17
14
  activities["day"] = [start.date() for start in activities["start"]]
@@ -1,5 +1,4 @@
1
1
  import datetime
2
- import functools
3
2
  import itertools
4
3
 
5
4
  import altair as alt
@@ -13,7 +12,6 @@ class EntryController:
13
12
  def __init__(self, repository: ActivityRepository) -> None:
14
13
  self._repository = repository
15
14
 
16
- @functools.cache
17
15
  def render(self) -> dict:
18
16
  result = {
19
17
  "distance_last_30_days_plot": distance_last_30_days_meta_plot(
@@ -1,5 +1,3 @@
1
- import functools
2
-
3
1
  import altair as alt
4
2
  import pandas as pd
5
3
 
@@ -11,7 +9,6 @@ class EquipmentController:
11
9
  def __init__(self, repository: ActivityRepository) -> None:
12
10
  self._repository = repository
13
11
 
14
- @functools.cache
15
12
  def render(self) -> dict:
16
13
  total_distances = (
17
14
  self._repository.meta.groupby("equipment")
@@ -1,5 +1,4 @@
1
1
  import datetime
2
- import functools
3
2
  import itertools
4
3
  import pickle
5
4
 
@@ -20,27 +19,24 @@ from geo_activity_playground.explorer.grid_file import make_explorer_tile
20
19
  from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
21
20
  from geo_activity_playground.explorer.grid_file import make_grid_file_gpx
22
21
  from geo_activity_playground.explorer.grid_file import make_grid_points
23
- from geo_activity_playground.explorer.tile_visits import TILE_EVOLUTION_STATES_PATH
24
- from geo_activity_playground.explorer.tile_visits import TILE_HISTORIES_PATH
25
- from geo_activity_playground.explorer.tile_visits import TILE_VISITS_PATH
26
22
  from geo_activity_playground.explorer.tile_visits import TileEvolutionState
23
+ from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
27
24
 
28
25
 
29
26
  alt.data_transformers.enable("vegafusion")
30
27
 
31
28
 
32
29
  class ExplorerController:
33
- def __init__(self, repository: ActivityRepository) -> None:
30
+ def __init__(
31
+ self, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
32
+ ) -> None:
34
33
  self._repository = repository
34
+ self._tile_visit_accessor = tile_visit_accessor
35
35
 
36
- @functools.cache
37
36
  def render(self, zoom: int) -> dict:
38
- with open(TILE_EVOLUTION_STATES_PATH, "rb") as f:
39
- tile_evolution_states = pickle.load(f)
40
- with open(TILE_VISITS_PATH, "rb") as f:
41
- tile_visits = pickle.load(f)
42
- with open(TILE_HISTORIES_PATH, "rb") as f:
43
- tile_histories = pickle.load(f)
37
+ tile_evolution_states = self._tile_visit_accessor.states
38
+ tile_visits = self._tile_visit_accessor.visits
39
+ tile_histories = self._tile_visit_accessor.histories
44
40
 
45
41
  medians = tile_histories[zoom].median()
46
42
  median_lat, median_lon = get_tile_upper_left_lat_lon(
@@ -79,8 +75,7 @@ class ExplorerController:
79
75
  x2, y2 = compute_tile(south, east, zoom)
80
76
  tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
81
77
 
82
- with open(TILE_HISTORIES_PATH, "rb") as f:
83
- tile_histories = pickle.load(f)
78
+ tile_histories = self._tile_visit_accessor.histories
84
79
  tiles = tile_histories[zoom]
85
80
  points = get_border_tiles(tiles, zoom, tile_bounds)
86
81
  if suffix == "geojson":
@@ -93,8 +88,7 @@ class ExplorerController:
93
88
  x2, y2 = compute_tile(south, east, zoom)
94
89
  tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
95
90
 
96
- with open(TILE_VISITS_PATH, "rb") as f:
97
- tile_visits = pickle.load(f)
91
+ tile_visits = self._tile_visit_accessor.visits
98
92
  tiles = tile_visits[zoom]
99
93
  points = make_grid_points(
100
94
  (tile for tile in tiles.keys() if tile_bounds.contains(*tile)), zoom
@@ -1,11 +1,8 @@
1
- import functools
2
1
  import io
3
2
  import logging
4
3
  import pathlib
5
4
  import pickle
6
- import threading
7
5
 
8
- import matplotlib
9
6
  import matplotlib.pylab as pl
10
7
  import numpy as np
11
8
  import pandas as pd
@@ -19,9 +16,7 @@ from geo_activity_playground.core.heatmap import get_sensible_zoom_level
19
16
  from geo_activity_playground.core.tasks import work_tracker
20
17
  from geo_activity_playground.core.tiles import get_tile
21
18
  from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
22
- from geo_activity_playground.explorer.tile_visits import TILE_EVOLUTION_STATES_PATH
23
- from geo_activity_playground.explorer.tile_visits import TILE_HISTORIES_PATH
24
- from geo_activity_playground.explorer.tile_visits import TILE_VISITS_PATH
19
+ from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
25
20
  from geo_activity_playground.webui.explorer_controller import (
26
21
  bounding_box_for_biggest_cluster,
27
22
  )
@@ -34,18 +29,16 @@ OSM_TILE_SIZE = 256 # OSM tile size in pixel
34
29
 
35
30
 
36
31
  class HeatmapController:
37
- def __init__(self, repository: ActivityRepository) -> None:
32
+ def __init__(
33
+ self, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
34
+ ) -> None:
38
35
  self._repository = repository
39
- self._all_points = pd.DataFrame()
36
+ self._tile_visit_accessor = tile_visit_accessor
40
37
 
41
- with open(TILE_HISTORIES_PATH, "rb") as f:
42
- self.tile_histories = pickle.load(f)
43
- with open(TILE_EVOLUTION_STATES_PATH, "rb") as f:
44
- self.tile_evolution_states = pickle.load(f)
45
- with open(TILE_VISITS_PATH, "rb") as f:
46
- self.tile_visits = pickle.load(f)
38
+ self.tile_histories = self._tile_visit_accessor.histories
39
+ self.tile_evolution_states = self._tile_visit_accessor.states
40
+ self.tile_visits = self._tile_visit_accessor.visits
47
41
 
48
- @functools.cache
49
42
  def render(self) -> dict:
50
43
  zoom = 14
51
44
  tiles = self.tile_histories[zoom]
@@ -58,11 +51,13 @@ class HeatmapController:
58
51
  "center": {
59
52
  "latitude": median_lat,
60
53
  "longitude": median_lon,
61
- "bbox": bounding_box_for_biggest_cluster(
62
- cluster_state.clusters.values(), zoom
63
- )
64
- if len(cluster_state.memberships) > 0
65
- else {},
54
+ "bbox": (
55
+ bounding_box_for_biggest_cluster(
56
+ cluster_state.clusters.values(), zoom
57
+ )
58
+ if len(cluster_state.memberships) > 0
59
+ else {}
60
+ ),
66
61
  }
67
62
  }
68
63
 
@@ -14,7 +14,6 @@ class SearchController:
14
14
  activities = []
15
15
  for _, row in self._repository.meta.iterrows():
16
16
  if name in row["name"]:
17
- print(row["name"])
18
17
  activities.append(
19
18
  {
20
19
  "name": row["name"],
@@ -22,7 +21,6 @@ class SearchController:
22
21
  "kind": row["kind"],
23
22
  "distance_km": row["distance_km"],
24
23
  "elapsed_time": row["elapsed_time"],
25
- "commute": row["commute"],
26
24
  }
27
25
  )
28
26
 
@@ -1,4 +1,3 @@
1
- import functools
2
1
  import pickle
3
2
 
4
3
  import geojson
@@ -9,15 +8,17 @@ from geo_activity_playground.explorer.grid_file import make_explorer_tile
9
8
  from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
10
9
  from geo_activity_playground.explorer.grid_file import make_grid_file_gpx
11
10
  from geo_activity_playground.explorer.grid_file import make_grid_points
12
- from geo_activity_playground.explorer.tile_visits import TILE_VISITS_PATH
11
+ from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
13
12
 
14
13
 
15
14
  class SquarePlannerController:
16
- def __init__(self, repository: ActivityRepository) -> None:
15
+ def __init__(
16
+ self, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
17
+ ) -> None:
17
18
  self._repository = repository
19
+ self._tile_visit_accessor = tile_visit_accessor
18
20
 
19
- with open(TILE_VISITS_PATH, "rb") as f:
20
- self._tile_visits = pickle.load(f)
21
+ self._tile_visits = self._tile_visit_accessor.visits
21
22
 
22
23
  def action_planner(
23
24
  self, zoom: int, square_x: int, square_y: int, square_size: int
@@ -79,11 +80,9 @@ class SquarePlannerController:
79
80
  elif suffix == "gpx":
80
81
  return make_grid_file_gpx(points)
81
82
 
82
- @functools.cache
83
83
  def _get_explored_tiles(self, zoom: int) -> set[tuple[int, int]]:
84
84
  return set(self._tile_visits[zoom].keys())
85
85
 
86
- @functools.cache
87
86
  def _get_explored_geojson(self, zoom: int) -> str:
88
87
  return geojson.dumps(
89
88
  geojson.FeatureCollection(
@@ -84,6 +84,9 @@
84
84
  <li class="nav-item">
85
85
  <a class="nav-link active" aria-current="page" href="/equipment">Equipment</a>
86
86
  </li>
87
+ <li class="nav-item">
88
+ <a class="nav-link active" aria-current="page" href="/upload">Upload</a>
89
+ </li>
87
90
  </ul>
88
91
  </div>
89
92
  </div>
@@ -26,7 +26,7 @@
26
26
  <td>{{ activity.name }}</td>
27
27
  <td>{{ activity.start }}</td>
28
28
  <td>{{ activity.kind }}</td>
29
- <td>{{ '%.1f' % activity["distance/km"] }} km</td>
29
+ <td>{{ '%.1f' % activity["distance_km"] }} km</td>
30
30
  <td>{{ activity.elapsed_time }}</td>
31
31
  <td>{{ activity.commute }}</td>
32
32
  </tr>
@@ -0,0 +1,36 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+ <div class="row mb-1">
5
+ <div class="col">
6
+ <h1>Upload Activity</h1>
7
+
8
+ <form method="post" enctype="multipart/form-data" action="/upload/receive">
9
+
10
+
11
+
12
+
13
+ <div class="mb-3">
14
+ <label for="file1" class="form-label">Activity file</label>
15
+ <input type="file" name="file" id="file1" class="form-control">
16
+ </div>
17
+
18
+
19
+ <div class="mb-3">
20
+ <label for="directory" class="form-label">Target directory</label>
21
+ <select name="directory" id="directory" class="form-select" aria-label="Default select example">
22
+ {% for directory in directories %}
23
+ <option>{{ directory }}</option>
24
+ {% endfor %}
25
+ </select>
26
+ </div>
27
+
28
+
29
+ <button type="submit" class="btn btn-primary">Upload</button>
30
+
31
+ </form>
32
+ </div>
33
+ </div>
34
+
35
+
36
+ {% endblock %}
@@ -0,0 +1,106 @@
1
+ import logging
2
+ import os
3
+ import pathlib
4
+ import sys
5
+
6
+ from flask import flash
7
+ from flask import redirect
8
+ from flask import request
9
+ from flask import Response
10
+ from flask import url_for
11
+ from werkzeug.utils import secure_filename
12
+
13
+ from geo_activity_playground.core.activities import ActivityRepository
14
+ from geo_activity_playground.core.activities import embellish_time_series
15
+ from geo_activity_playground.explorer.tile_visits import compute_tile_evolution
16
+ from geo_activity_playground.explorer.tile_visits import compute_tile_visits
17
+ from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
18
+ from geo_activity_playground.importers.directory import get_file_hash
19
+ from geo_activity_playground.importers.directory import import_from_directory
20
+ from geo_activity_playground.importers.strava_api import import_from_strava_api
21
+ from geo_activity_playground.importers.strava_checkout import (
22
+ import_from_strava_checkout,
23
+ )
24
+
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class UploadController:
30
+ def __init__(
31
+ self,
32
+ repository: ActivityRepository,
33
+ tile_visit_accessor: TileVisitAccessor,
34
+ config: dict,
35
+ ) -> None:
36
+ self._repository = repository
37
+ self._tile_visit_accessor = tile_visit_accessor
38
+ self._config = config
39
+
40
+ def render_form(self) -> dict:
41
+ directories = []
42
+ for root, dirs, files in os.walk("Activities"):
43
+ directories.append(root)
44
+ directories.sort()
45
+ return {"directories": directories}
46
+
47
+ def receive(self) -> Response:
48
+ # check if the post request has the file part
49
+ if "file" not in request.files:
50
+ flash("No file part")
51
+ return redirect(request.url)
52
+ file = request.files["file"]
53
+ # If the user does not select a file, the browser submits an
54
+ # empty file without a filename.
55
+ if file.filename == "":
56
+ flash("No selected file")
57
+ return redirect(request.url)
58
+ if file:
59
+ filename = secure_filename(file.filename)
60
+ target_path = pathlib.Path(request.form["directory"]) / filename
61
+ assert target_path.suffix in [
62
+ ".csv",
63
+ ".fit",
64
+ ".gpx",
65
+ ".gz",
66
+ ".kml",
67
+ ".kmz",
68
+ ".tcx",
69
+ ]
70
+ assert target_path.is_relative_to("Activities")
71
+ file.save(target_path)
72
+ scan_for_activities(
73
+ self._repository,
74
+ self._tile_visit_accessor,
75
+ self._config,
76
+ skip_strava=True,
77
+ )
78
+ activity_id = get_file_hash(target_path)
79
+ return redirect(f"/activity/{activity_id}")
80
+
81
+
82
+ def scan_for_activities(
83
+ repository: ActivityRepository,
84
+ tile_visit_accessor: TileVisitAccessor,
85
+ config: dict,
86
+ skip_strava: bool = False,
87
+ ) -> None:
88
+ if pathlib.Path("Activities").exists():
89
+ import_from_directory(
90
+ repository,
91
+ config.get("metadata_extraction_regexes", []),
92
+ )
93
+ if pathlib.Path("Strava Export").exists():
94
+ import_from_strava_checkout(repository)
95
+ if "strava" in config and not skip_strava:
96
+ import_from_strava_api(repository)
97
+
98
+ if len(repository) == 0:
99
+ logger.error(
100
+ f"No activities found. You need to either add activity files (GPX, FIT, …) to {pathlib.Path('Activities')} or set up the Strava API. Starting without any activities is unfortunately not supported."
101
+ )
102
+ sys.exit(1)
103
+
104
+ embellish_time_series(repository)
105
+ compute_tile_visits(repository, tile_visit_accessor)
106
+ compute_tile_evolution(tile_visit_accessor)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "geo-activity-playground"
3
- version = "0.21.1"
3
+ version = "0.22.0"
4
4
  description = "Analysis of geo data activities like rides, runs or hikes."
5
5
  authors = ["Martin Ueding <mu@martin-ueding.de>"]
6
6
  license = "MIT"
@@ -13,6 +13,7 @@ python = "^3.9,<3.13"
13
13
 
14
14
  altair = "^5.1.2"
15
15
  appdirs = "^1.4.4"
16
+ charset-normalizer = "^3.3.2"
16
17
  coloredlogs = "^15.0.1"
17
18
  fitdecode = "^0.10.0"
18
19
  flask = "^3.0.0"
@@ -35,22 +36,27 @@ vegafusion = { version = "^1.4.3", extras = ["embed"] }
35
36
  vegafusion-python-embed = "^1.4.3"
36
37
  vl-convert-python = "^1.0.1"
37
38
  xmltodict = "^0.13.0"
38
- charset-normalizer = "^3.3.2"
39
39
 
40
40
  [tool.poetry.group.dev.dependencies]
41
41
  black = "^22.3.0"
42
42
  mkdocs-material = "^9.4.1"
43
43
  mypy = "^1.10.0"
44
- pytest = "^7.1.2"
44
+ pandas-stubs = "^2.2.2.240603"
45
45
  py-spy = "^0.3.14"
46
- types-pyyaml = "^6.0.12.12"
46
+ pytest = "^7.1.2"
47
47
  types-decorator = "^5.1.8.20240106"
48
48
  types-paramiko = "^3.4.0.20240120"
49
49
  types-pycurl = "^7.45.2.20240106"
50
50
  types-pytz = "^2024.1.0.20240203"
51
+ types-pyyaml = "^6.0.12.12"
51
52
  types-requests = "^2.31.0.20240125"
53
+ types-tqdm = "^4.66.0.20240417"
52
54
  types-typed-ast = "^1.5.8.7"
55
+ types-xmltodict = "^0.13.0.3"
53
56
 
54
57
  [build-system]
55
58
  requires = ["poetry-core>=1.0.0"]
56
59
  build-backend = "poetry.core.masonry.api"
60
+
61
+ [tool.mypy]
62
+ disable_error_code = "import-untyped"