geovisio 2.8.1__py3-none-any.whl → 2.10.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 (70) hide show
  1. geovisio/__init__.py +6 -1
  2. geovisio/config_app.py +16 -5
  3. geovisio/translations/ar/LC_MESSAGES/messages.mo +0 -0
  4. geovisio/translations/ar/LC_MESSAGES/messages.po +818 -0
  5. geovisio/translations/br/LC_MESSAGES/messages.po +1 -1
  6. geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
  7. geovisio/translations/da/LC_MESSAGES/messages.po +4 -3
  8. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/de/LC_MESSAGES/messages.po +55 -2
  10. geovisio/translations/el/LC_MESSAGES/messages.po +1 -1
  11. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  12. geovisio/translations/en/LC_MESSAGES/messages.po +193 -139
  13. geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
  14. geovisio/translations/eo/LC_MESSAGES/messages.po +53 -4
  15. geovisio/translations/es/LC_MESSAGES/messages.po +1 -1
  16. geovisio/translations/fi/LC_MESSAGES/messages.po +1 -1
  17. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  18. geovisio/translations/fr/LC_MESSAGES/messages.po +101 -6
  19. geovisio/translations/hu/LC_MESSAGES/messages.po +1 -1
  20. geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/it/LC_MESSAGES/messages.po +63 -3
  22. geovisio/translations/ja/LC_MESSAGES/messages.po +1 -1
  23. geovisio/translations/ko/LC_MESSAGES/messages.po +1 -1
  24. geovisio/translations/messages.pot +185 -129
  25. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  26. geovisio/translations/nl/LC_MESSAGES/messages.po +421 -86
  27. geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
  28. geovisio/translations/oc/LC_MESSAGES/messages.po +818 -0
  29. geovisio/translations/pl/LC_MESSAGES/messages.po +1 -1
  30. geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
  31. geovisio/translations/sv/LC_MESSAGES/messages.po +823 -0
  32. geovisio/translations/ti/LC_MESSAGES/messages.mo +0 -0
  33. geovisio/translations/ti/LC_MESSAGES/messages.po +762 -0
  34. geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +1 -1
  35. geovisio/utils/annotations.py +183 -0
  36. geovisio/utils/auth.py +14 -13
  37. geovisio/utils/cql2.py +134 -0
  38. geovisio/utils/db.py +7 -0
  39. geovisio/utils/fields.py +38 -9
  40. geovisio/utils/items.py +44 -0
  41. geovisio/utils/model_query.py +4 -4
  42. geovisio/utils/pic_shape.py +63 -0
  43. geovisio/utils/pictures.py +164 -29
  44. geovisio/utils/reports.py +10 -17
  45. geovisio/utils/semantics.py +196 -57
  46. geovisio/utils/sentry.py +1 -2
  47. geovisio/utils/sequences.py +191 -93
  48. geovisio/utils/tags.py +31 -0
  49. geovisio/utils/upload_set.py +287 -209
  50. geovisio/utils/website.py +1 -1
  51. geovisio/web/annotations.py +346 -9
  52. geovisio/web/auth.py +1 -1
  53. geovisio/web/collections.py +73 -54
  54. geovisio/web/configuration.py +26 -5
  55. geovisio/web/docs.py +143 -11
  56. geovisio/web/items.py +232 -155
  57. geovisio/web/map.py +25 -13
  58. geovisio/web/params.py +55 -52
  59. geovisio/web/pictures.py +34 -0
  60. geovisio/web/stac.py +19 -12
  61. geovisio/web/tokens.py +49 -1
  62. geovisio/web/upload_set.py +148 -37
  63. geovisio/web/users.py +4 -4
  64. geovisio/web/utils.py +2 -2
  65. geovisio/workers/runner_pictures.py +190 -24
  66. {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/METADATA +27 -26
  67. geovisio-2.10.0.dist-info/RECORD +105 -0
  68. {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/WHEEL +1 -1
  69. geovisio-2.8.1.dist-info/RECORD +0 -92
  70. {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/licenses/LICENSE +0 -0
geovisio/web/map.py CHANGED
@@ -352,23 +352,35 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
352
352
  sql.SQL("nb_360_pictures"),
353
353
  sql.SQL("nb_pictures - nb_360_pictures AS nb_flat_pictures"),
354
354
  sql.SQL(
355
- """((CASE WHEN nb_pictures = 0 THEN 0 WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
356
- THEN nb_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid) * 0.5
357
- ELSE 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
355
+ """((CASE WHEN nb_pictures = 0
356
+ THEN 0
357
+ WHEN nb_pictures <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) FILTER (WHERE nb_pictures > 0) FROM pictures_grid)
358
+ THEN
359
+ nb_pictures::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) FILTER (WHERE nb_pictures > 0) FROM pictures_grid) * 0.5
360
+ ELSE
361
+ 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
358
362
  END) * 10)::int / 10::float AS coef"""
359
363
  ),
360
364
  sql.SQL(
361
- """((CASE WHEN nb_360_pictures = 0 THEN 0
362
- WHEN nb_360_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid)
363
- THEN nb_360_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid) * 0.5
364
- ELSE 0.5 + nb_360_pictures::float / (SELECT MAX(nb_360_pictures) FROM pictures_grid) * 0.5
365
+ """((CASE WHEN nb_360_pictures = 0
366
+ THEN 0
367
+ WHEN (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid) = 0
368
+ THEN 0
369
+ WHEN nb_360_pictures <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid)
370
+ THEN nb_360_pictures::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid) * 0.5
371
+ ELSE
372
+ 0.5 + nb_360_pictures::float / (SELECT MAX(nb_360_pictures) FROM pictures_grid) * 0.5
365
373
  END) * 10)::int / 10::float AS coef_360_pictures"""
366
374
  ),
367
375
  sql.SQL(
368
- """((CASE WHEN (nb_pictures - nb_360_pictures) = 0 THEN 0
369
- WHEN (nb_pictures - nb_360_pictures) <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid)
370
- THEN (nb_pictures - nb_360_pictures)::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid) * 0.5
371
- ELSE 0.5 + (nb_pictures - nb_360_pictures)::float / (SELECT MAX((nb_pictures - nb_360_pictures)) FROM pictures_grid) * 0.5
376
+ """((CASE WHEN (nb_pictures - nb_360_pictures) = 0
377
+ THEN 0
378
+ WHEN (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid) = 0
379
+ THEN 0
380
+ WHEN (nb_pictures - nb_360_pictures) <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid)
381
+ THEN (nb_pictures - nb_360_pictures)::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid) * 0.5
382
+ ELSE
383
+ 0.5 + (nb_pictures - nb_360_pictures)::float / (SELECT MAX((nb_pictures - nb_360_pictures)) FROM pictures_grid) * 0.5
372
384
  END) * 10)::int / 10::float AS coef_flat_pictures"""
373
385
  ),
374
386
  ]
@@ -616,7 +628,7 @@ def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
616
628
  format: binary
617
629
  """
618
630
 
619
- filter = params.parse_filter(request.args.get("filter"))
631
+ filter = params.parse_collection_filter(request.args.get("filter"))
620
632
  return _getTile(z, x, y, format, onlyForUser=userId, filter=filter)
621
633
 
622
634
 
@@ -688,5 +700,5 @@ def getMyTile(account: Account, z: int, x: int, y: int, format: str):
688
700
  type: string
689
701
  format: binary
690
702
  """
691
- filter = params.parse_filter(request.args.get("filter"))
703
+ filter = params.parse_collection_filter(request.args.get("filter"))
692
704
  return _getTile(z, x, y, format, onlyForUser=UUID(account.id), filter=filter)
geovisio/web/params.py CHANGED
@@ -8,14 +8,15 @@ import datetime
8
8
  import re
9
9
  from werkzeug.datastructures import MultiDict
10
10
  from typing import Optional, Tuple, Any, List
11
- from pygeofilter.backends.sql import to_sql_where
12
- from pygeofilter.parsers.ecql import parse as ecql_parser
13
11
  from pygeofilter import ast
14
12
  from pygeofilter.backends.evaluator import Evaluator, handle
15
13
  from psycopg import sql
16
14
  from geovisio.utils.sequences import STAC_FIELD_MAPPINGS, STAC_FIELD_TO_SQL_FILTER
17
15
  from geovisio.utils.fields import SortBy, SQLDirection, SortByField
18
16
  from flask_babel import gettext as _
17
+ from geovisio.utils import items as utils_items
18
+
19
+ from geovisio.utils.cql2 import parse_cql2_filter
19
20
 
20
21
 
21
22
  RGX_SORTBY = re.compile("[+-]?[A-Za-z_].*(,[+-]?[A-Za-z_].*)*")
@@ -43,11 +44,11 @@ def parse_datetime(value, error, fallback_as_UTC=False):
43
44
 
44
45
  """
45
46
  # Hack to parse a date
46
- # dateutils know how to parse lots of date, but fail to correctly parse date formated by `datetime.isoformat()`
47
+ # dateutils know how to parse lots of date, but fail to correctly parse date formatted by `datetime.isoformat()`
47
48
  # (like all the dates returned by the API).
48
49
  # datetime.isoformat is like: `2023-06-17T21:22:18.406856+02:00`
49
- # dateutils silently fails the parse, and create an incorect date
50
- # so we first try to parse it like an isoformated date, and if this fails we try the flexible dateutils
50
+ # dateutils silently fails the parse, and create an incorrect date
51
+ # so we first try to parse it like an isoformatted date, and if this fails we try the flexible dateutils
51
52
  d = None
52
53
  try:
53
54
  d = datetime.datetime.fromisoformat(value)
@@ -325,37 +326,27 @@ def parse_list(value: Optional[Any], tryFallbacks: bool = True, paramName: Optio
325
326
  return None
326
327
 
327
328
 
328
- def parse_filter(value: Optional[str]) -> Optional[sql.SQL]:
329
+ def parse_collection_filter(value: Optional[str]) -> Optional[sql.SQL]:
329
330
  """Reads STAC filter parameter and sends SQL condition back.
330
331
 
331
- >>> parse_filter('')
332
+ >>> parse_collection_filter('')
332
333
 
333
- >>> parse_filter("updated >= '2023-12-31'")
334
+ >>> parse_collection_filter("updated >= '2023-12-31'")
334
335
  SQL("(s.updated_at >= '2023-12-31')")
335
- >>> parse_filter("updated >= '2023-12-31' AND created < '2023-10-31'")
336
+ >>> parse_collection_filter("updated >= '2023-12-31' AND created < '2023-10-31'")
336
337
  SQL("((s.updated_at >= '2023-12-31') AND (s.inserted_at < '2023-10-31'))")
337
- >>> parse_filter("status IN ('deleted','ready')") # when we ask for deleted, we should also have hidden collections
338
+ >>> parse_collection_filter("status IN ('deleted','ready')") # when we ask for deleted, we should also have hidden collections
338
339
  SQL("s.status IN ('deleted', 'ready', 'hidden')")
339
- >>> parse_filter("status = 'deleted' OR status = 'ready'")
340
+ >>> parse_collection_filter("status = 'deleted' OR status = 'ready'")
340
341
  SQL("(((s.status = 'deleted') OR (s.status = 'hidden')) OR (s.status = 'ready'))")
341
- >>> parse_filter('invalid = 10') # doctest: +IGNORE_EXCEPTION_DETAIL
342
+ >>> parse_collection_filter('invalid = 10') # doctest: +IGNORE_EXCEPTION_DETAIL
342
343
  Traceback (most recent call last):
343
344
  geovisio.errors.InvalidAPIUsage: Unsupported filter parameter
344
- >>> parse_filter('updated == 10') # doctest: +IGNORE_EXCEPTION_DETAIL
345
+ >>> parse_collection_filter('updated == 10') # doctest: +IGNORE_EXCEPTION_DETAIL
345
346
  Traceback (most recent call last):
346
347
  geovisio.errors.InvalidAPIUsage: Unsupported filter parameter
347
348
  """
348
- if value is not None and len(value) > 0:
349
- try:
350
- filterAst = ecql_parser(value)
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
355
- except:
356
- raise errors.InvalidAPIUsage(_("Unsupported filter parameter"), status_code=400)
357
- else:
358
- return None
349
+ return parse_cql2_filter(value, STAC_FIELD_TO_SQL_FILTER, ast_updater=_alterFilterAst)
359
350
 
360
351
 
361
352
  def parse_picture_heading(heading: Optional[str]) -> Optional[int]:
@@ -410,7 +401,25 @@ def _alterFilterAst(ast: ast.Node):
410
401
  return filtered
411
402
 
412
403
 
413
- def parse_sortby(value: Optional[str]) -> Optional[SortBy]:
404
+ def _parse_sorty_by(value: Optional[str], field_mapping_func, SortByCls):
405
+ if not value:
406
+ return None
407
+ # Check value pattern
408
+ if not RGX_SORTBY.match(value):
409
+ raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: syntax isn't correct"), status_code=400)
410
+ values = value.split(",")
411
+ orders = []
412
+ for v in values:
413
+ direction = SQLDirection.DESC if v.startswith("-") else SQLDirection.ASC
414
+ raw_field = v.lstrip("+-")
415
+ f = field_mapping_func(raw_field, direction)
416
+
417
+ orders.append(f)
418
+
419
+ return SortByCls(fields=orders)
420
+
421
+
422
+ def parse_collection_sortby(value: Optional[str]) -> Optional[SortBy]:
414
423
  """Reads STAC/OGC sortby parameter, and sends a SQL ORDER BY string.
415
424
 
416
425
  Parameters
@@ -426,46 +435,40 @@ def parse_sortby(value: Optional[str]) -> Optional[SortBy]:
426
435
 
427
436
  None if no sort by is found
428
437
 
429
- >>> parse_sortby(None)
430
- >>> parse_sortby("")
431
- >>> parse_sortby('updated')
438
+ >>> parse_collection_sortby(None)
439
+ >>> parse_collection_sortby("")
440
+ >>> parse_collection_sortby('updated')
432
441
  SortBy(fields=[SortByField(field=FieldMapping(sql_column=SQL('updated_at'), stac='updated'), direction=<SQLDirection.ASC: SQL('ASC')>)])
433
- >>> parse_sortby('+created')
442
+ >>> parse_collection_sortby('+created')
434
443
  SortBy(fields=[SortByField(field=FieldMapping(sql_column=SQL('inserted_at'), stac='created'), direction=<SQLDirection.ASC: SQL('ASC')>)])
435
- >>> parse_sortby('-created')
444
+ >>> parse_collection_sortby('-created')
436
445
  SortBy(fields=[SortByField(field=FieldMapping(sql_column=SQL('inserted_at'), stac='created'), direction=<SQLDirection.DESC: SQL('DESC')>)])
437
- >>> parse_sortby('+updated,-created')
446
+ >>> parse_collection_sortby('+updated,-created')
438
447
  SortBy(fields=[SortByField(field=FieldMapping(sql_column=SQL('updated_at'), stac='updated'), direction=<SQLDirection.ASC: SQL('ASC')>), SortByField(field=FieldMapping(sql_column=SQL('inserted_at'), stac='created'), direction=<SQLDirection.DESC: SQL('DESC')>)])
439
- >>> parse_sortby('invalid') # doctest: +IGNORE_EXCEPTION_DETAIL
448
+ >>> parse_collection_sortby('invalid') # doctest: +IGNORE_EXCEPTION_DETAIL
440
449
  Traceback (most recent call last):
441
450
  geovisio.errors.InvalidAPIUsage: Unsupported sortby parameter
442
- >>> parse_sortby('~nb') # doctest: +IGNORE_EXCEPTION_DETAIL
451
+ >>> parse_collection_sortby('~nb') # doctest: +IGNORE_EXCEPTION_DETAIL
443
452
  Traceback (most recent call last):
444
453
  geovisio.errors.InvalidAPIUsage: Unsupported sortby parameter
445
454
  """
446
455
 
447
- if value is not None and len(value) > 0:
448
- # Check value pattern
449
- if RGX_SORTBY.match(value):
450
- values = value.split(",")
451
- orders = []
452
- for v in values:
453
- direction = SQLDirection.DESC if v.startswith("-") else SQLDirection.ASC
454
- vOnly = v.replace("+", "").replace("-", "")
456
+ def mapping(raw_field: str, direction: SQLDirection):
457
+ if raw_field not in STAC_FIELD_MAPPINGS:
458
+ raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: invalid column name"), status_code=400)
459
+ return SortByField(field=STAC_FIELD_MAPPINGS[raw_field], direction=direction)
455
460
 
456
- # Check if in value mapping
457
- if vOnly not in STAC_FIELD_MAPPINGS:
458
- raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: invalid column name"), status_code=400)
459
- field_mapping = STAC_FIELD_MAPPINGS[vOnly]
461
+ return _parse_sorty_by(value, mapping, SortByCls=SortBy)
460
462
 
461
- orders.append(SortByField(field=field_mapping, direction=direction))
462
463
 
463
- # Create definitive ORDER string
464
- return SortBy(fields=orders)
465
- else:
466
- raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: syntax isn't correct"), status_code=400)
467
- else:
468
- return None
464
+ def parse_item_sortby(value: Optional[str]) -> Optional[utils_items.SortBy]:
465
+ def mapping(raw_field: str, direction: SQLDirection):
466
+ if raw_field == "distance_to" or raw_field not in utils_items.SortableItemField.__dict__:
467
+ # distance to is for the moment only an implicit sort when search a point or in a bbox
468
+ raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: invalid field"), status_code=400)
469
+ return utils_items.ItemSortByField(field=utils_items.SortableItemField[raw_field], direction=direction)
470
+
471
+ return _parse_sorty_by(value, mapping, SortByCls=utils_items.SortBy)
469
472
 
470
473
 
471
474
  def parse_collections_limit(limit: Optional[str]) -> int:
geovisio/web/pictures.py CHANGED
@@ -208,3 +208,37 @@ def getPictureTile(pictureId, col, row, format):
208
208
  raise errors.InvalidAPIUsage(_("Unable to read picture on filesystem"), status_code=500)
209
209
 
210
210
  return utils.pictures.sendInFormat(picture, "jpeg", format)
211
+
212
+
213
+ @bp.route("/<uuid:pictureId>")
214
+ def getPictureById(pictureId):
215
+ """Get picture's STAC definition.
216
+
217
+ It's the non-stac alias to the `/api/collections/<collectionId>/items/<itemId>` endpoint (but you don't need to know the collection ID here).
218
+ ---
219
+ tags:
220
+ - Pictures
221
+ parameters:
222
+ - name: pictureId
223
+ in: path
224
+ description: ID of the picture (called item in STAC) to retrieve
225
+ required: true
226
+ schema:
227
+ type: string
228
+ responses:
229
+ 102:
230
+ description: the picture (which is still under process)
231
+ content:
232
+ application/geo+json:
233
+ schema:
234
+ $ref: '#/components/schemas/GeoVisioItem'
235
+ 200:
236
+ description: the wanted picture
237
+ content:
238
+ application/geo+json:
239
+ schema:
240
+ $ref: '#/components/schemas/GeoVisioItem'
241
+ """
242
+ from geovisio.web.items import getCollectionItem
243
+
244
+ return getCollectionItem(collectionId=None, itemId=pictureId)
geovisio/web/stac.py CHANGED
@@ -20,10 +20,11 @@ from geovisio.utils.sequences import (
20
20
  get_collections,
21
21
  CollectionsRequest,
22
22
  STAC_FIELD_MAPPINGS,
23
+ get_dataset_bounds,
23
24
  get_pagination_links,
24
25
  )
25
26
  from geovisio.web.params import (
26
- parse_filter,
27
+ parse_collection_filter,
27
28
  parse_collections_limit,
28
29
  )
29
30
 
@@ -334,12 +335,17 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
334
335
  """
335
336
 
336
337
  collection_request = CollectionsRequest(
337
- sort_by=SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC)]),
338
+ sort_by=SortBy(
339
+ fields=[
340
+ SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC),
341
+ SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC),
342
+ ]
343
+ ),
338
344
  user_id=userId,
339
345
  userOwnsAllCollections=userIdMatchesAccount,
340
346
  )
341
347
  collection_request.limit = parse_collections_limit(request.args.get("limit"))
342
- collection_request.pagination_filter = parse_filter(request.args.get("page"))
348
+ collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
343
349
 
344
350
  userName = None
345
351
  meta_collection = None
@@ -350,12 +356,14 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
350
356
  raise errors.InvalidAPIUsage(_("Impossible to find user %(u)s", u=userId))
351
357
  userName = userName["name"]
352
358
 
353
- meta_collection = cursor.execute(
354
- SQL("SELECT MIN(inserted_at) AS min_order, MAX(inserted_at) AS max_order FROM sequences s WHERE account_id = %(account)s"),
355
- params={"account": userId},
356
- ).fetchone()
359
+ datasetBounds = get_dataset_bounds(
360
+ cursor.connection,
361
+ collection_request.sort_by,
362
+ additional_filters=SQL("s.account_id = %(account)s"),
363
+ additional_filters_params={"account": userId},
364
+ )
357
365
 
358
- if not meta_collection or meta_collection["min_order"] is None:
366
+ if datasetBounds is None:
359
367
  # No data found, trying to give the most meaningfull error message
360
368
  raise errors.InvalidAPIUsage(_("No data loaded for user %(u)s", u=userId), 404)
361
369
 
@@ -388,10 +396,9 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
388
396
  pagination_links = get_pagination_links(
389
397
  route="stac.getUserCatalog",
390
398
  routeArgs={"userId": str(userId), "limit": collection_request.limit},
391
- field=collection_request.sort_by.fields[0].field.stac,
392
- direction=collection_request.sort_by.fields[0].direction,
393
- datasetBounds=Bounds(min=meta_collection["min_order"], max=meta_collection["max_order"]),
394
- dataBounds=db_collections.query_first_order_bounds,
399
+ sortBy=collection_request.sort_by,
400
+ datasetBounds=datasetBounds,
401
+ dataBounds=db_collections.query_bounds,
395
402
  additional_filters=None,
396
403
  )
397
404
 
geovisio/web/tokens.py CHANGED
@@ -22,7 +22,7 @@ def list_tokens(account):
22
22
 
23
23
  The list of tokens will not contain their JWT counterpart (the JWT is the real token used in authentication).
24
24
 
25
- The JWT counterpart can be retreived by providing the token's id to the endpoint [/users/me/tokens/{token_id}](#/Auth/get_api_users_me_tokens__token_id_).
25
+ The JWT counterpart can be retrieved by providing the token's id to the endpoint [/users/me/tokens/{token_id}](#/Auth/get_api_users_me_tokens__token_id_).
26
26
  ---
27
27
  tags:
28
28
  - Auth
@@ -254,6 +254,54 @@ def claim_non_associated_token(token_id, account):
254
254
  return "You are now logged in the CLI, you can upload your pictures", 200
255
255
 
256
256
 
257
+ @bp.route("/users/me/tokens", methods=["POST"])
258
+ @auth.login_required_with_redirect()
259
+ def generate_associated_token(account: auth.Account):
260
+ """
261
+ Generate a new token associated to the current user
262
+
263
+ The response contains the JWT token and is directly usable (unlike tokens created by `/auth/tokens/generate` that are not associated to a user by default). This token does not need to be claimed.
264
+ ---
265
+ tags:
266
+ - Auth
267
+ requestBody:
268
+ content:
269
+ application/json:
270
+ schema:
271
+ $ref: '#/components/schemas/GeovisioPostToken'
272
+ responses:
273
+ 200:
274
+ description: The newly generated token
275
+ content:
276
+ application/json:
277
+ schema:
278
+ $ref: '#/components/schemas/GeoVisioEncodedToken'
279
+ """
280
+ if request.is_json:
281
+ description = request.json.get("description", "")
282
+ else:
283
+ description = None
284
+
285
+ token = db.fetchone(
286
+ current_app,
287
+ "INSERT INTO tokens (description, account_id) VALUES (%(description)s, %(account_id)s) RETURNING *",
288
+ {"account_id": account.id, "description": description},
289
+ row_factory=dict_row,
290
+ )
291
+ if not token:
292
+ raise errors.InternalError(_("Impossible to generate a new token"))
293
+
294
+ jwt_token = _generate_jwt_token(token["id"])
295
+ return flask.jsonify(
296
+ {
297
+ "jwt_token": jwt_token,
298
+ "id": token["id"],
299
+ "description": token["description"],
300
+ "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
301
+ }
302
+ )
303
+
304
+
257
305
  def _generate_jwt_token(token_id: uuid.UUID) -> str:
258
306
  """
259
307
  Generate a JWT token from a token's id.