geovisio 2.5.0__py3-none-any.whl → 2.7.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 (59) hide show
  1. geovisio/__init__.py +38 -8
  2. geovisio/admin_cli/__init__.py +2 -2
  3. geovisio/admin_cli/db.py +8 -0
  4. geovisio/config_app.py +64 -0
  5. geovisio/db_migrations.py +24 -3
  6. geovisio/templates/main.html +14 -14
  7. geovisio/templates/viewer.html +3 -3
  8. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/de/LC_MESSAGES/messages.po +667 -0
  10. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  11. geovisio/translations/en/LC_MESSAGES/messages.po +730 -0
  12. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
  14. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
  16. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
  18. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
  20. geovisio/translations/messages.pot +686 -0
  21. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  22. geovisio/translations/nl/LC_MESSAGES/messages.po +594 -0
  23. geovisio/utils/__init__.py +1 -1
  24. geovisio/utils/auth.py +50 -11
  25. geovisio/utils/db.py +65 -0
  26. geovisio/utils/excluded_areas.py +83 -0
  27. geovisio/utils/extent.py +30 -0
  28. geovisio/utils/fields.py +1 -1
  29. geovisio/utils/filesystems.py +0 -1
  30. geovisio/utils/link.py +14 -0
  31. geovisio/utils/params.py +20 -0
  32. geovisio/utils/pictures.py +94 -69
  33. geovisio/utils/reports.py +171 -0
  34. geovisio/utils/sequences.py +288 -126
  35. geovisio/utils/tokens.py +37 -42
  36. geovisio/utils/upload_set.py +654 -0
  37. geovisio/web/auth.py +50 -37
  38. geovisio/web/collections.py +305 -319
  39. geovisio/web/configuration.py +14 -0
  40. geovisio/web/docs.py +288 -12
  41. geovisio/web/excluded_areas.py +377 -0
  42. geovisio/web/items.py +203 -151
  43. geovisio/web/map.py +322 -106
  44. geovisio/web/params.py +69 -26
  45. geovisio/web/pictures.py +14 -31
  46. geovisio/web/reports.py +399 -0
  47. geovisio/web/rss.py +13 -7
  48. geovisio/web/stac.py +129 -121
  49. geovisio/web/tokens.py +105 -112
  50. geovisio/web/upload_set.py +768 -0
  51. geovisio/web/users.py +100 -73
  52. geovisio/web/utils.py +38 -9
  53. geovisio/workers/runner_pictures.py +278 -183
  54. geovisio-2.7.0.dist-info/METADATA +95 -0
  55. geovisio-2.7.0.dist-info/RECORD +66 -0
  56. geovisio-2.5.0.dist-info/METADATA +0 -115
  57. geovisio-2.5.0.dist-info/RECORD +0 -41
  58. {geovisio-2.5.0.dist-info → geovisio-2.7.0.dist-info}/LICENSE +0 -0
  59. {geovisio-2.5.0.dist-info → geovisio-2.7.0.dist-info}/WHEEL +0 -0
geovisio/web/params.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from uuid import UUID
2
+
2
3
  from geovisio import errors
3
4
  import dateutil.parser
4
5
  from dateutil import tz
@@ -9,9 +10,12 @@ from werkzeug.datastructures import MultiDict
9
10
  from typing import Optional, Tuple, Any, List
10
11
  from pygeofilter.backends.sql import to_sql_where
11
12
  from pygeofilter.parsers.ecql import parse as ecql_parser
13
+ from pygeofilter import ast
14
+ from pygeofilter.backends.evaluator import Evaluator, handle
12
15
  from psycopg import sql
13
16
  from geovisio.utils.sequences import STAC_FIELD_MAPPINGS, STAC_FIELD_TO_SQL_FILTER
14
17
  from geovisio.utils.fields import SortBy, SQLDirection, SortByField
18
+ from flask_babel import gettext as _
15
19
 
16
20
 
17
21
  RGX_SORTBY = re.compile("[+-]?[A-Za-z_].*(,[+-]?[A-Za-z_].*)*")
@@ -47,7 +51,7 @@ def parse_datetime(value, error, fallback_as_UTC=False):
47
51
  d = None
48
52
  try:
49
53
  d = datetime.datetime.fromisoformat(value)
50
- except ValueError as e:
54
+ except ValueError:
51
55
  pass
52
56
  if not d:
53
57
  try:
@@ -81,17 +85,17 @@ def parse_datetime_interval(value: Optional[str]) -> Tuple[Optional[datetime.dat
81
85
  dates = value.split("/")
82
86
 
83
87
  if len(dates) == 1:
84
- d = parse_datetime(dates[0], error=f"Invalid `datetime` argument", fallback_as_UTC=True)
88
+ d = parse_datetime(dates[0], error="Invalid `datetime` argument", fallback_as_UTC=True)
85
89
  return (d, d)
86
90
 
87
91
  elif len(dates) == 2:
88
92
  # Check if interval is closed or open-ended
89
93
  mind, maxd = dates
90
- mind = None if mind == ".." else parse_datetime(mind, error=f"Invalid start date in `datetime` argument", fallback_as_UTC=True)
91
- maxd = None if maxd == ".." else parse_datetime(maxd, error=f"Invalid end date in `datetime` argument", fallback_as_UTC=True)
94
+ mind = None if mind == ".." else parse_datetime(mind, error="Invalid start date in `datetime` argument", fallback_as_UTC=True)
95
+ maxd = None if maxd == ".." else parse_datetime(maxd, error="Invalid end date in `datetime` argument", fallback_as_UTC=True)
92
96
  return (mind, maxd)
93
97
  else:
94
- raise errors.InvalidAPIUsage("Parameter datetime should contain one or two dates", status_code=400)
98
+ raise errors.InvalidAPIUsage(_("Parameter datetime should contain one or two dates"), status_code=400)
95
99
 
96
100
 
97
101
  def parse_bbox(value: Optional[Any], tryFallbacks=True):
@@ -161,12 +165,12 @@ def parse_bbox(value: Optional[Any], tryFallbacks=True):
161
165
  or bbox[3] > 90
162
166
  ):
163
167
  raise errors.InvalidAPIUsage(
164
- "Parameter bbox must contain valid longitude (-180 to 180) and latitude (-90 to 90) values", status_code=400
168
+ _("Parameter bbox must contain valid longitude (-180 to 180) and latitude (-90 to 90) values"), status_code=400
165
169
  )
166
170
  else:
167
171
  return bbox
168
172
  except ValueError:
169
- raise errors.InvalidAPIUsage("Parameter bbox must be in format [minX, minY, maxX, maxY]", status_code=400)
173
+ raise errors.InvalidAPIUsage(_("Parameter bbox must be in format [minX, minY, maxX, maxY]"), status_code=400)
170
174
  else:
171
175
  return None
172
176
 
@@ -203,11 +207,11 @@ def parse_lonlat(values: Optional[Any], paramName: Optional[str] = None) -> Opti
203
207
  entries = parse_list(values, paramName=paramName)
204
208
 
205
209
  if entries is None or len(entries) != 2:
206
- raise errors.InvalidAPIUsage(f"Parameter {paramName or ''} must be coordinates in lat,lon format", status_code=400)
210
+ raise errors.InvalidAPIUsage(_("Parameter %(p)s must be coordinates in lat,lon format", p=paramName or ""), status_code=400)
207
211
 
208
212
  return [
209
- as_longitude(entries[0], f"Longitude in parameter {paramName or ''} is not valid (should be between -180 and 180)"),
210
- as_latitude(entries[1], f"Latitude in parameter {paramName or ''} is not valid (should be between -90 and 90)"),
213
+ as_longitude(entries[0], _("Longitude in parameter %(p)s is not valid (should be between -180 and 180)", p=paramName or "")),
214
+ as_latitude(entries[1], _("Latitude in parameter %(p)s is not valid (should be between -90 and 90)", p=paramName or "")),
211
215
  ]
212
216
 
213
217
 
@@ -230,18 +234,20 @@ def parse_distance_range(values: Optional[str], paramName: Optional[str] = None)
230
234
  dists = values.split("-")
231
235
  if len(dists) != 2:
232
236
  raise errors.InvalidAPIUsage(
233
- f"Parameter {paramName or ''} is invalid (should be a distance range in meters like \"5-15\")", status_code=400
237
+ _('Parameter %(p)s is invalid (should be a distance range in meters like "5-15")', p={paramName or ""}), status_code=400
234
238
  )
235
239
  try:
236
240
  dists = [int(d) for d in dists]
237
241
  if dists[0] > dists[1]:
238
- raise errors.InvalidAPIUsage(f"Parameter {paramName or ''} has a min value greater than its max value", status_code=400)
242
+ raise errors.InvalidAPIUsage(
243
+ _("Parameter %(p)s has a min value greater than its max value", p=paramName or ""), status_code=400
244
+ )
239
245
  else:
240
246
  return dists
241
247
 
242
248
  except ValueError:
243
249
  raise errors.InvalidAPIUsage(
244
- f"Parameter {paramName or ''} is invalid (should be a distance range in meters like \"5-15\")", status_code=400
250
+ _('Parameter %(p)s is invalid (should be a distance range in meters like "5-15")', p={paramName or ""}), status_code=400
245
251
  )
246
252
  else:
247
253
  return None
@@ -308,7 +314,7 @@ def parse_list(value: Optional[Any], tryFallbacks: bool = True, paramName: Optio
308
314
  value = value.replace("[", "").replace("]", "")
309
315
  res = [n.strip() for n in value.split(",")]
310
316
  else:
311
- raise errors.InvalidAPIUsage(f"Parameter {paramName or ''} must be a valid list", status_code=400)
317
+ raise errors.InvalidAPIUsage(_("Parameter %(p)s must be a valid list", p=paramName or ""), status_code=400)
312
318
 
313
319
  if len(res) == 0:
314
320
  return None
@@ -328,10 +334,10 @@ def parse_filter(value: Optional[str]) -> Optional[sql.SQL]:
328
334
  SQL("(s.updated_at >= '2023-12-31')")
329
335
  >>> parse_filter("updated >= '2023-12-31' AND created < '2023-10-31'")
330
336
  SQL("((s.updated_at >= '2023-12-31') AND (s.inserted_at < '2023-10-31'))")
331
- >>> parse_filter("status IN ('deleted','ready')")
332
- SQL("s.status IN ('deleted', 'ready')")
337
+ >>> parse_filter("status IN ('deleted','ready')") # when we ask for deleted, we should also have hidden collections
338
+ SQL("s.status IN ('deleted', 'ready', 'hidden')")
333
339
  >>> parse_filter("status = 'deleted' OR status = 'ready'")
334
- SQL("((s.status = 'deleted') OR (s.status = 'ready'))")
340
+ SQL("(((s.status = 'deleted') OR (s.status = 'hidden')) OR (s.status = 'ready'))")
335
341
  >>> parse_filter('invalid = 10') # doctest: +IGNORE_EXCEPTION_DETAIL
336
342
  Traceback (most recent call last):
337
343
  geovisio.errors.InvalidAPIUsage: Unsupported filter parameter
@@ -342,14 +348,51 @@ def parse_filter(value: Optional[str]) -> Optional[sql.SQL]:
342
348
  if value is not None and len(value) > 0:
343
349
  try:
344
350
  filterAst = ecql_parser(value)
345
- f = to_sql_where(filterAst, STAC_FIELD_TO_SQL_FILTER).replace('"', "")
346
- return sql.SQL(f)
351
+ altered_ast = _alterFilterAst(filterAst) # type: ignore
352
+
353
+ f = to_sql_where(altered_ast, STAC_FIELD_TO_SQL_FILTER).replace('"', "")
354
+ return sql.SQL(f) # type: ignore
347
355
  except:
348
- raise errors.InvalidAPIUsage(f"Unsupported filter parameter", status_code=400)
356
+ raise errors.InvalidAPIUsage(_("Unsupported filter parameter"), status_code=400)
349
357
  else:
350
358
  return None
351
359
 
352
360
 
361
+ class _FilterAstUpdated(Evaluator):
362
+ """
363
+ We alter the parsed AST in order to always query for 'hidden' pictures when we query for 'deleted' ones
364
+
365
+ The rational here is that for non-owned pictures/sequences, when a pictures/sequence is 'hidden' it should be advertised as 'deleted'.
366
+
367
+ This is especially important for crawler like the meta-catalog, since they should also delete the sequence/picture when it is hidden
368
+ """
369
+
370
+ @handle(ast.Equal)
371
+ def eq(self, node, lhs, rhs):
372
+ if lhs == ast.Attribute("status") and rhs == "deleted":
373
+ return ast.Or(node, ast.Equal(ast.Attribute("status"), "hidden")) # type: ignore
374
+ return node
375
+
376
+ @handle(ast.Or)
377
+ def or_(self, node, lhs, rhs):
378
+ return ast.Or(lhs, rhs)
379
+
380
+ @handle(ast.In)
381
+ def in_(self, node, lhs, *options):
382
+ if "deleted" in node.sub_nodes:
383
+ node.sub_nodes.append("hidden")
384
+ return node
385
+
386
+ def adopt(self, node, *sub_args):
387
+ return node
388
+
389
+
390
+ def _alterFilterAst(ast: ast.Node):
391
+ filtered = _FilterAstUpdated().evaluate(ast)
392
+
393
+ return filtered
394
+
395
+
353
396
  def parse_sortby(value: Optional[str]) -> Optional[SortBy]:
354
397
  """Reads STAC/OGC sortby parameter, and sends a SQL ORDER BY string.
355
398
 
@@ -395,7 +438,7 @@ def parse_sortby(value: Optional[str]) -> Optional[SortBy]:
395
438
 
396
439
  # Check if in value mapping
397
440
  if vOnly not in STAC_FIELD_MAPPINGS:
398
- raise errors.InvalidAPIUsage(f"Unsupported sortby parameter: invalid column name", status_code=400)
441
+ raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: invalid column name"), status_code=400)
399
442
  field_mapping = STAC_FIELD_MAPPINGS[vOnly]
400
443
 
401
444
  orders.append(SortByField(field=field_mapping, direction=direction))
@@ -403,7 +446,7 @@ def parse_sortby(value: Optional[str]) -> Optional[SortBy]:
403
446
  # Create definitive ORDER string
404
447
  return SortBy(fields=orders)
405
448
  else:
406
- raise errors.InvalidAPIUsage(f"Unsupported sortby parameter: syntax isn't correct", status_code=400)
449
+ raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: syntax isn't correct"), status_code=400)
407
450
  else:
408
451
  return None
409
452
 
@@ -429,10 +472,10 @@ def parse_collections_limit(limit: Optional[str]) -> int:
429
472
  try:
430
473
  int_limit = int(limit)
431
474
  except ValueError:
432
- raise errors.InvalidAPIUsage(f"limit parameter should be a valid, positive integer (between 1 and {SEQUENCES_MAX_FETCH})")
475
+ raise errors.InvalidAPIUsage(_("limit parameter should be a valid, positive integer (between 1 and %(v)s)", v=SEQUENCES_MAX_FETCH))
433
476
 
434
477
  if int_limit < 1 or int_limit > SEQUENCES_MAX_FETCH:
435
- raise errors.InvalidAPIUsage(f"limit parameter should be an integer between 1 and {SEQUENCES_MAX_FETCH}")
478
+ raise errors.InvalidAPIUsage(_("limit parameter should be an integer between 1 and %(v)s", v=SEQUENCES_MAX_FETCH))
436
479
  else:
437
480
  return int_limit
438
481
 
@@ -443,7 +486,7 @@ def as_longitude(value: str, error):
443
486
  except ValueError as e:
444
487
  raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": str(e)}})
445
488
  if l < -180 or l > 180:
446
- raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": "longitude needs to be between -180 and 180"}})
489
+ raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": _("longitude needs to be between -180 and 180")}})
447
490
  return l
448
491
 
449
492
 
@@ -453,7 +496,7 @@ def as_latitude(value: str, error):
453
496
  except ValueError as e:
454
497
  raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": str(e)}})
455
498
  if l < -90 or l > 90:
456
- raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": "latitude needs to be between -90 and 90"}})
499
+ raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": _("latitude needs to be between -90 and 90")}})
457
500
  return l
458
501
 
459
502
 
geovisio/web/pictures.py CHANGED
@@ -1,8 +1,7 @@
1
- import os
2
- from itertools import repeat
3
- from flask import Blueprint, current_app, request
1
+ from flask import Blueprint, current_app
4
2
  from geovisio import utils, errors
5
3
  from flask import redirect
4
+ from flask_babel import gettext as _
6
5
  import logging
7
6
 
8
7
  bp = Blueprint("pictures", __name__, url_prefix="/api/pictures")
@@ -25,7 +24,7 @@ def getPictureHD(pictureId, format):
25
24
  type: string
26
25
  - name: format
27
26
  in: path
28
- description: Wanted format for output image (either jpg or webp)
27
+ description: Wanted format for output image (for the moment only jpg)
29
28
  required: true
30
29
  schema:
31
30
  type: string
@@ -37,10 +36,6 @@ def getPictureHD(pictureId, format):
37
36
  schema:
38
37
  type: string
39
38
  format: binary
40
- image/webp:
41
- schema:
42
- type: string
43
- format: binary
44
39
  """
45
40
 
46
41
  utils.pictures.checkFormatParam(format)
@@ -55,7 +50,7 @@ def getPictureHD(pictureId, format):
55
50
  try:
56
51
  picture = fses.permanent.openbin(utils.pictures.getHDPicturePath(pictureId))
57
52
  except:
58
- raise errors.InvalidAPIUsage("Unable to read picture on filesystem", status_code=500)
53
+ raise errors.InvalidAPIUsage(_("Unable to read picture on filesystem"), status_code=500)
59
54
 
60
55
  return utils.pictures.sendInFormat(picture, "jpeg", format)
61
56
 
@@ -75,7 +70,7 @@ def getPictureSD(pictureId, format):
75
70
  type: string
76
71
  - name: format
77
72
  in: path
78
- description: Wanted format for output image (either jpg or webp)
73
+ description: Wanted format for output image (for the moment only jpg)
79
74
  required: true
80
75
  schema:
81
76
  type: string
@@ -87,10 +82,6 @@ def getPictureSD(pictureId, format):
87
82
  schema:
88
83
  type: string
89
84
  format: binary
90
- image/webp:
91
- schema:
92
- type: string
93
- format: binary
94
85
  """
95
86
  utils.pictures.checkFormatParam(format)
96
87
 
@@ -104,7 +95,7 @@ def getPictureSD(pictureId, format):
104
95
  try:
105
96
  picture = fses.derivates.openbin(utils.pictures.getPictureFolderPath(pictureId) + "/sd.jpg")
106
97
  except:
107
- raise errors.InvalidAPIUsage("Unable to read picture on filesystem", status_code=500)
98
+ raise errors.InvalidAPIUsage(_("Unable to read picture on filesystem"), status_code=500)
108
99
 
109
100
  return utils.pictures.sendInFormat(picture, "jpeg", format)
110
101
 
@@ -124,7 +115,7 @@ def getPictureThumb(pictureId, format):
124
115
  type: string
125
116
  - name: format
126
117
  in: path
127
- description: Wanted format for output image (either jpg or webp)
118
+ description: Wanted format for output image (for the moment only jpg)
128
119
  required: true
129
120
  schema:
130
121
  type: string
@@ -136,10 +127,6 @@ def getPictureThumb(pictureId, format):
136
127
  schema:
137
128
  type: string
138
129
  format: binary
139
- image/webp:
140
- schema:
141
- type: string
142
- format: binary
143
130
  """
144
131
  return utils.pictures.sendThumbnail(pictureId, format)
145
132
 
@@ -171,7 +158,7 @@ def getPictureTile(pictureId, col, row, format):
171
158
  type: number
172
159
  - name: format
173
160
  in: path
174
- description: Wanted format for output image (either jpg or webp)
161
+ description: Wanted format for output image (for the moment only jpg)
175
162
  required: true
176
163
  schema:
177
164
  type: string
@@ -183,10 +170,6 @@ def getPictureTile(pictureId, col, row, format):
183
170
  schema:
184
171
  type: string
185
172
  format: binary
186
- image/webp:
187
- schema:
188
- type: string
189
- format: binary
190
173
  """
191
174
 
192
175
  utils.pictures.checkFormatParam(format)
@@ -201,27 +184,27 @@ def getPictureTile(pictureId, col, row, format):
201
184
  picPath = f"{utils.pictures.getPictureFolderPath(pictureId)}/tiles/{col}_{row}.jpg"
202
185
 
203
186
  if metadata["type"] == "flat":
204
- raise errors.InvalidAPIUsage("Tiles are not available for flat pictures", status_code=404)
187
+ raise errors.InvalidAPIUsage(_("Tiles are not available for flat pictures"), status_code=404)
205
188
 
206
189
  try:
207
190
  col = int(col)
208
191
  except:
209
- raise errors.InvalidAPIUsage("Column parameter is invalid, should be an integer", status_code=404)
192
+ raise errors.InvalidAPIUsage(_("Column parameter is invalid, should be an integer"), status_code=404)
210
193
 
211
194
  if col < 0 or col >= metadata["cols"]:
212
- raise errors.InvalidAPIUsage("Column parameter is invalid", status_code=404)
195
+ raise errors.InvalidAPIUsage(_("Column parameter is invalid"), status_code=404)
213
196
 
214
197
  try:
215
198
  row = int(row)
216
199
  except:
217
- raise errors.InvalidAPIUsage("Row parameter is invalid, should be an integer", status_code=404)
200
+ raise errors.InvalidAPIUsage(_("Row parameter is invalid, should be an integer"), status_code=404)
218
201
 
219
202
  if row < 0 or row >= metadata["rows"]:
220
- raise errors.InvalidAPIUsage("Row parameter is invalid", status_code=404)
203
+ raise errors.InvalidAPIUsage(_("Row parameter is invalid"), status_code=404)
221
204
 
222
205
  try:
223
206
  picture = fses.derivates.openbin(picPath)
224
207
  except:
225
- raise errors.InvalidAPIUsage("Unable to read picture on filesystem", status_code=500)
208
+ raise errors.InvalidAPIUsage(_("Unable to read picture on filesystem"), status_code=500)
226
209
 
227
210
  return utils.pictures.sendInFormat(picture, "jpeg", format)