geo-activity-playground 0.45.0__py3-none-any.whl → 1.0.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.
@@ -229,6 +229,9 @@ def query_activity_meta(clauses: list = []) -> pd.DataFrame:
229
229
  .order_by(Activity.start)
230
230
  ).all()
231
231
  df = pd.DataFrame(rows)
232
+ # If the search yields only activities without time information, the dtype isn't derived correctly.
233
+ df["start"] = pd.to_datetime(df["start"])
234
+ df["elapsed_time"] = pd.to_timedelta(df["elapsed_time"])
232
235
 
233
236
  if len(df):
234
237
  for old, new in [
@@ -0,0 +1,129 @@
1
+ import io
2
+ import zipfile
3
+ from typing import IO
4
+
5
+ import geojson
6
+ import gpxpy.gpx
7
+ import pandas as pd
8
+ import sqlalchemy
9
+ from tqdm import tqdm
10
+
11
+ from .datamodel import Activity
12
+ from .datamodel import DB
13
+ from .datamodel import query_activity_meta
14
+
15
+
16
+ def export_all(meta_format: str, activity_format: str) -> bytes:
17
+ meta = query_activity_meta()
18
+ buffer = io.BytesIO()
19
+ with zipfile.ZipFile(buffer, "x") as zf:
20
+ with zf.open(f"activities.{meta_format}", mode="w") as f:
21
+ match meta_format:
22
+ case "csv":
23
+ export_meta_as_csv(meta, f)
24
+ case "json":
25
+ export_meta_as_json(meta, f)
26
+ case "ods":
27
+ export_meta_as_xlsx(meta, f)
28
+ case "parquet":
29
+ export_meta_as_parquet(meta, f)
30
+ case "xlsx":
31
+ export_meta_as_xlsx(meta, f)
32
+ case _:
33
+ raise ValueError(
34
+ f"Format {meta_format} is not supported for metadata."
35
+ )
36
+ if activity_format:
37
+ zf.mkdir("activities")
38
+ for activity in tqdm(
39
+ DB.session.scalars(sqlalchemy.select(Activity)).all(),
40
+ desc="Export activity time series",
41
+ ):
42
+ with zf.open(
43
+ f"activities/{activity.id}.{activity_format}", mode="w"
44
+ ) as f:
45
+ match activity_format:
46
+ case "csv":
47
+ export_activity_as_csv(activity, f)
48
+ case "geojson":
49
+ export_activity_as_geojson(activity, f)
50
+ case "gpx":
51
+ export_activity_as_gpx(activity, f)
52
+ case "ods":
53
+ export_activity_as_xlsx(activity, f)
54
+ case "parquet":
55
+ export_activity_as_parquet(activity, f)
56
+ case "xlsx":
57
+ export_activity_as_xlsx(activity, f)
58
+ case _:
59
+ raise ValueError(
60
+ f"Format {activity_format} is not supported for activity time series."
61
+ )
62
+ return bytes(buffer.getbuffer())
63
+
64
+
65
+ def export_meta_as_csv(meta: pd.DataFrame, target: IO[bytes]) -> None:
66
+ meta.to_csv(target, index=False)
67
+
68
+
69
+ def export_meta_as_json(meta: pd.DataFrame, target: IO[bytes]) -> None:
70
+ buffer = io.StringIO()
71
+ meta.to_json(buffer, index=False)
72
+ target.write(buffer.getvalue().encode())
73
+
74
+
75
+ def export_meta_as_parquet(meta: pd.DataFrame, target: IO[bytes]) -> None:
76
+ meta.to_parquet(target, index=False)
77
+
78
+
79
+ def export_meta_as_xlsx(meta: pd.DataFrame, target: IO[bytes]) -> None:
80
+ meta.to_excel(target, index=False)
81
+
82
+
83
+ def export_activity_as_csv(activity: Activity, target: IO[bytes]) -> None:
84
+ activity.time_series.to_csv(target, index=False)
85
+
86
+
87
+ def export_activity_as_geojson(activity: Activity, target: IO[bytes]) -> None:
88
+ ts = activity.time_series
89
+ result = geojson.MultiLineString(
90
+ coordinates=[
91
+ [(lon, lat) for lat, lon in zip(group["latitude"], group["longitude"])]
92
+ for segment_id, group in ts.groupby("segment_id")
93
+ ]
94
+ )
95
+ buffer = io.StringIO()
96
+ geojson.dump(result, buffer)
97
+ target.write(buffer.getvalue().encode())
98
+
99
+
100
+ def export_activity_as_gpx(activity: Activity, target: IO[bytes]) -> None:
101
+ g = gpxpy.gpx.GPX()
102
+
103
+ gpx_track = gpxpy.gpx.GPXTrack()
104
+ g.tracks.append(gpx_track)
105
+
106
+ ts = activity.time_series
107
+ for segment_id, group in ts.groupby("segment_id"):
108
+ gpx_segment = gpxpy.gpx.GPXTrackSegment()
109
+ gpx_track.segments.append(gpx_segment)
110
+
111
+ for index, row in group.iterrows():
112
+ gpx_segment.points.append(
113
+ gpxpy.gpx.GPXTrackPoint(
114
+ row["latitude"],
115
+ row["longitude"],
116
+ elevation=row.get("elevation", None),
117
+ time=row.get("time", None),
118
+ )
119
+ )
120
+
121
+ target.write(g.to_xml().encode())
122
+
123
+
124
+ def export_activity_as_parquet(activity: Activity, target: IO[bytes]) -> None:
125
+ activity.time_series.to_parquet(target, index=False)
126
+
127
+
128
+ def export_activity_as_xlsx(activity: Activity, target: IO[bytes]) -> None:
129
+ activity.time_series.to_excel(target, index=False)
@@ -158,7 +158,7 @@ def apply_search_query(
158
158
  )
159
159
 
160
160
  if search_query.start_begin:
161
- filter_clauses.append(Activity.start <= search_query.start_begin)
161
+ filter_clauses.append(Activity.start >= search_query.start_begin)
162
162
  if search_query.start_end:
163
163
  filter_clauses.append(Activity.start < search_query.start_end)
164
164
 
@@ -38,6 +38,7 @@ from .blueprints.eddington_blueprints import register_eddington_blueprint
38
38
  from .blueprints.entry_views import register_entry_views
39
39
  from .blueprints.equipment_blueprint import make_equipment_blueprint
40
40
  from .blueprints.explorer_blueprint import make_explorer_blueprint
41
+ from .blueprints.export_blueprint import make_export_blueprint
41
42
  from .blueprints.heatmap_blueprint import make_heatmap_blueprint
42
43
  from .blueprints.photo_blueprint import make_photo_blueprint
43
44
  from .blueprints.plot_builder_blueprint import make_plot_builder_blueprint
@@ -160,6 +161,7 @@ def web_ui_main(
160
161
  tile_getter,
161
162
  image_transforms,
162
163
  ),
164
+ "/export": make_export_blueprint(authenticator),
163
165
  "/heatmap": make_heatmap_blueprint(
164
166
  repository, tile_visit_accessor, config_accessor(), search_query_history
165
167
  ),
@@ -175,7 +177,7 @@ def web_ui_main(
175
177
  "/summary": make_summary_blueprint(repository, config, search_query_history),
176
178
  "/tile": make_tile_blueprint(image_transforms, tile_getter),
177
179
  "/upload": make_upload_blueprint(
178
- repository, tile_visit_accessor, config_accessor(), authenticator
180
+ repository, tile_visit_accessor, config_accessor(), authenticator, flasher
179
181
  ),
180
182
  }
181
183
 
@@ -3,8 +3,10 @@ from typing import Callable
3
3
 
4
4
  from flask import flash
5
5
  from flask import redirect
6
+ from flask import request
6
7
  from flask import session
7
8
  from flask import url_for
9
+ from flask.typing import RouteCallable
8
10
 
9
11
  from ..core.config import Config
10
12
 
@@ -32,14 +34,14 @@ class Authenticator:
32
34
 
33
35
 
34
36
  def needs_authentication(authenticator: Authenticator) -> Callable:
35
- def decorator(route: Callable) -> Callable:
37
+ def decorator(route: RouteCallable) -> RouteCallable:
36
38
  @functools.wraps(route)
37
39
  def wrapped_route(*args, **kwargs):
38
40
  if authenticator.is_authenticated():
39
41
  return route(*args, **kwargs)
40
42
  else:
41
43
  flash("You need to be logged in to view that site.", category="Warning")
42
- return redirect(url_for("auth.index"))
44
+ return redirect(url_for("auth.index", redirect=request.url))
43
45
 
44
46
  return wrapped_route
45
47
 
@@ -14,9 +14,12 @@ def make_auth_blueprint(authenticator: Authenticator) -> Blueprint:
14
14
  def index():
15
15
  if request.method == "POST":
16
16
  authenticator.authenticate(request.form["password"])
17
+ if redirect_to := request.form["redirect"]:
18
+ return redirect(redirect_to)
17
19
  return render_template(
18
20
  "auth/index.html.j2",
19
21
  is_authenticated=authenticator.is_authenticated(),
22
+ redirect=request.args.get("redirect", ""),
20
23
  )
21
24
 
22
25
  @blueprint.route("/logout")