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
@@ -1,23 +1,24 @@
1
1
  from enum import Enum
2
- from attr import dataclass
3
2
  from geovisio import errors, utils, db
4
3
  from geovisio.utils import auth, sequences
5
4
  from geovisio.utils.params import validation_error
6
- from geovisio.utils.semantics import SemanticTagUpdate, Entity, EntityType, update_tags
5
+ from geovisio.utils.semantics import Entity, EntityType, update_tags
6
+ from geovisio.utils.tags import SemanticTagUpdate
7
7
  from geovisio.web.params import (
8
8
  parse_datetime,
9
9
  parse_datetime_interval,
10
10
  parse_bbox,
11
- parse_filter,
12
- parse_sortby,
11
+ parse_collection_filter,
12
+ parse_collection_sortby,
13
13
  parse_collections_limit,
14
14
  )
15
15
  from geovisio.utils.sequences import (
16
16
  STAC_FIELD_MAPPINGS,
17
17
  CollectionsRequest,
18
18
  get_collections,
19
+ get_dataset_bounds,
19
20
  )
20
- from geovisio.utils.fields import SortBy, SortByField, SQLDirection, Bounds, BBox
21
+ from geovisio.utils.fields import SortBy, SortByField, SQLDirection, BBox, parse_relative_heading
21
22
  from geovisio.web.rss import dbSequencesToGeoRSS
22
23
  from psycopg.rows import dict_row
23
24
  from psycopg.sql import SQL
@@ -94,7 +95,7 @@ def dbSequenceToStacCollection(dbSeq, description="A sequence of geolocated pict
94
95
  "title": str(dbSeq["name"]),
95
96
  "description": description,
96
97
  "keywords": ["pictures", str(dbSeq["name"])],
97
- "semantics": dbSeq["semantics"] if "semantics" in dbSeq else None,
98
+ "semantics": dbSeq.get("semantics", []),
98
99
  "license": current_app.config["API_PICTURES_LICENSE_SPDX_ID"],
99
100
  "created": dbTsToStac(dbSeq["created"]),
100
101
  "updated": dbTsToStac(dbSeq.get("updated")),
@@ -157,6 +158,16 @@ def dbSequenceToStacCollection(dbSeq, description="A sequence of geolocated pict
157
158
  "href": url_for("stac_collections.getCollection", _external=True, collectionId=dbSeq["id"]),
158
159
  },
159
160
  get_license_link(),
161
+ (
162
+ {
163
+ "rel": "upload_set",
164
+ "type": "application/json",
165
+ "title": "Link to the upload set",
166
+ "href": url_for("upload_set.getUploadSet", _external=True, upload_set_id=dbSeq["upload_set_id"]),
167
+ }
168
+ if dbSeq.get("upload_set_id")
169
+ else None
170
+ ),
160
171
  ]
161
172
  ),
162
173
  }
@@ -244,16 +255,22 @@ def getAllCollections():
244
255
  format = "rss"
245
256
 
246
257
  # Sort-by parameter
247
- sortBy = parse_sortby(request.args.get("sortby"))
258
+ sortBy = parse_collection_sortby(request.args.get("sortby"))
248
259
  if not sortBy:
249
260
  direction = SQLDirection.DESC if format == "rss" else SQLDirection.ASC
250
261
  sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=direction)])
262
+ # we always add the creation date fields in the sort list (after the selected ones), this will we'll get the `created` bounds of the dataset
263
+ # we'll also get
264
+ if not any(s.field == STAC_FIELD_MAPPINGS["created"] for s in sortBy.fields):
265
+ sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC))
266
+ if not any(s.field == STAC_FIELD_MAPPINGS["id"] for s in sortBy.fields):
267
+ sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC))
251
268
 
252
269
  collection_request = CollectionsRequest(sort_by=sortBy)
253
270
 
254
271
  # Filter parameter
255
- collection_request.user_filter = parse_filter(request.args.get("filter"))
256
- collection_request.pagination_filter = parse_filter(request.args.get("page"))
272
+ collection_request.user_filter = parse_collection_filter(request.args.get("filter"))
273
+ collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
257
274
 
258
275
  # Limit parameter
259
276
  collection_request.limit = parse_collections_limit(request.args.get("limit"))
@@ -298,14 +315,14 @@ def getAllCollections():
298
315
  },
299
316
  ]
300
317
 
301
- with db.cursor(current_app, row_factory=dict_row) as cursor:
302
- stats = cursor.execute("SELECT min(inserted_at) as min, max(inserted_at) as max FROM sequences").fetchone()
303
- if stats is None:
318
+ with db.conn(current_app) as conn:
319
+ datasetBounds = get_dataset_bounds(conn, collection_request.sort_by, additional_filters=collection_request.user_filter)
320
+ if datasetBounds is None:
304
321
  return ({"collections": [], "links": links}, 200, {"Content-Type": "application/json"})
305
- datasetBounds = Bounds(min=stats["min"], max=stats["max"])
306
- if collection_request.created_after and collection_request.created_after > datasetBounds.max:
322
+ creation_date_index = collection_request.sort_by.get_field_index("created")
323
+ if collection_request.created_after and collection_request.created_after > datasetBounds.last[creation_date_index]:
307
324
  raise errors.InvalidAPIUsage(_("There is no collection created after %(d)s", d=collection_request.created_after))
308
- if collection_request.created_before and collection_request.created_before < datasetBounds.min:
325
+ if collection_request.created_before and collection_request.created_before < datasetBounds.first[creation_date_index]:
309
326
  raise errors.InvalidAPIUsage(_("There is no collection created before %(d)s", d=collection_request.created_before))
310
327
 
311
328
  db_collections = get_collections(collection_request)
@@ -322,10 +339,9 @@ def getAllCollections():
322
339
  pagination_links = sequences.get_pagination_links(
323
340
  route="stac_collections.getAllCollections",
324
341
  routeArgs={"limit": collection_request.limit},
325
- field=sortBy.fields[0].field.stac,
326
- direction=sortBy.fields[0].direction,
342
+ sortBy=sortBy,
327
343
  datasetBounds=datasetBounds,
328
- dataBounds=db_collections.query_first_order_bounds,
344
+ dataBounds=db_collections.query_bounds,
329
345
  additional_filters=additional_filters,
330
346
  )
331
347
 
@@ -374,8 +390,7 @@ def getCollection(collectionId):
374
390
 
375
391
  record = db.fetchone(
376
392
  current_app,
377
- """
378
- SELECT
393
+ """SELECT
379
394
  s.id,
380
395
  s.metadata->>'title' AS name,
381
396
  ST_XMin(s.bbox) AS minx,
@@ -385,6 +400,7 @@ def getCollection(collectionId):
385
400
  s.status AS status,
386
401
  accounts.name AS account_name,
387
402
  s.account_id AS account_id,
403
+ s.upload_set_id,
388
404
  s.inserted_at AS created,
389
405
  s.updated_at AS updated,
390
406
  s.current_sort AS current_sort,
@@ -396,16 +412,16 @@ def getCollection(collectionId):
396
412
  ROUND(ST_Length(s.geom::geography)) / 1000 as length_km,
397
413
  s.computed_h_pixel_density,
398
414
  s.computed_gps_accuracy,
399
- t.semantics
415
+ COALESCE(seq_sem.semantics, '[]'::json) AS semantics
400
416
  FROM sequences s
401
417
  LEFT JOIN (
402
418
  SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
403
419
  'key', key,
404
420
  'value', value
405
- ))) AS semantics
421
+ )) ORDER BY key, value) AS semantics
406
422
  FROM sequences_semantics
407
423
  GROUP BY sequence_id
408
- ) t ON t.sequence_id = s.id
424
+ ) seq_sem ON seq_sem.sequence_id = s.id
409
425
  JOIN accounts ON s.account_id = accounts.id, (
410
426
  SELECT
411
427
  array_agg(DISTINCT jsonb_build_object(
@@ -494,6 +510,10 @@ def getCollectionThumbnail(collectionId):
494
510
  @auth.login_required_by_setting("API_FORCE_AUTH_ON_UPLOAD")
495
511
  def postCollection(account=None):
496
512
  """Create a new sequence
513
+
514
+ Note that this is the legacy API, upload should be done using the [UploadSet](#UploadSet) endpoints if possible.
515
+
516
+ Using an upload set makes it possible to handle more use cases like dispatching pictures into several collections, removing capture duplicates, parralele upload, ...
497
517
  ---
498
518
  tags:
499
519
  - Upload
@@ -503,7 +523,7 @@ def postCollection(account=None):
503
523
  required: false
504
524
  schema:
505
525
  type: string
506
- description: An explicit User-Agent value is prefered if you create a production-ready tool, formatted like "PanoramaxCLI/1.0"
526
+ description: An explicit User-Agent value is preferred if you create a production-ready tool, formatted like "PanoramaxCLI/1.0"
507
527
  requestBody:
508
528
  content:
509
529
  application/json:
@@ -557,7 +577,7 @@ class PatchCollectionParameter(BaseModel):
557
577
  """Parameters used to add an item to an UploadSet"""
558
578
 
559
579
  relative_heading: Optional[int] = None
560
- """The relative heading (in degrees), offset based on movement path (0° = looking forward, -90° = looking left, 90° = looking right). Headings are unchanged if this parameter is not set."""
580
+ """The relative heading (in degrees), offset based on movement path (0° = looking forward, -90° = looking left, 90° = looking right). For single picture collections, 0° is heading north). Headings are unchanged if this parameter is not set."""
561
581
  visible: Optional[bool] = None
562
582
  """Should the sequence be publicly visible ?"""
563
583
  title: Optional[str] = Field(max_length=250, default=None)
@@ -602,15 +622,7 @@ If unset, sort order is unchanged."""
602
622
  @field_validator("relative_heading", mode="before")
603
623
  @classmethod
604
624
  def parse_relative_heading(cls, value):
605
- try:
606
- relHeading = int(value)
607
- if relHeading < -180 or relHeading > 180:
608
- raise ValueError()
609
- return relHeading
610
- except ValueError:
611
- raise errors.InvalidAPIUsage(
612
- _("Relative heading is not valid, should be an integer in degrees from -180 to 180"), status_code=400
613
- )
625
+ return parse_relative_heading(value)
614
626
 
615
627
  def has_only_semantics_updates(self):
616
628
  return self.model_fields_set == {"semantics"}
@@ -629,7 +641,7 @@ def patchCollection(collectionId, account):
629
641
  ---
630
642
  tags:
631
643
  - Editing
632
- - Tags
644
+ - Semantics
633
645
  parameters:
634
646
  - name: collectionId
635
647
  in: path
@@ -749,13 +761,7 @@ def patchCollection(collectionId, account):
749
761
  sqlUpdates.append(SQL("last_account_to_edit = %(account)s"))
750
762
 
751
763
  cursor.execute(
752
- SQL(
753
- """
754
- UPDATE sequences
755
- SET {updates}
756
- WHERE id = %(id)s
757
- """
758
- ).format(updates=SQL(", ").join(sqlUpdates)),
764
+ SQL("UPDATE sequences SET {updates} WHERE id = %(id)s").format(updates=SQL(", ").join(sqlUpdates)),
759
765
  sqlParams,
760
766
  )
761
767
 
@@ -924,7 +930,12 @@ def send_collections_as_csv(collection_request: CollectionsRequest):
924
930
  raise errors.InvalidAPIUsage(_("CSV export does not support pagination"), status_code=400)
925
931
  if collection_request.filters():
926
932
  raise errors.InvalidAPIUsage(_("CSV export does not support filters"), status_code=400)
927
- if collection_request.sort_by != SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC)]):
933
+ if collection_request.sort_by != SortBy(
934
+ fields=[
935
+ SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC),
936
+ SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC),
937
+ ]
938
+ ):
928
939
  raise errors.InvalidAPIUsage(_("CSV export does not support sorting by anything but creation date"), status_code=400)
929
940
 
930
941
  def generate_csv():
@@ -957,7 +968,7 @@ SELECT
957
968
  s.computed_gps_accuracy AS computed_gps_accuracy
958
969
  FROM sequences s
959
970
  WHERE {filter}
960
- ORDER BY s.inserted_at DESC
971
+ ORDER BY s.inserted_at DESC, id ASC
961
972
  ) TO STDOUT CSV HEADER"""
962
973
  ).format(filter=SQL(" AND ").join(filters)),
963
974
  params,
@@ -966,7 +977,7 @@ ORDER BY s.inserted_at DESC
966
977
  for a in copy:
967
978
  yield bytes(a)
968
979
 
969
- return stream_with_context(generate_csv()), {"Content-Disposition": "attachment"}
980
+ return stream_with_context(generate_csv()), {"Content-Type": "text/csv", "Content-Disposition": "attachment"}
970
981
 
971
982
 
972
983
  @bp.route("/users/<uuid:userId>/collection")
@@ -1029,17 +1040,21 @@ def getUserCollection(userId, userIdMatchesAccount=False):
1029
1040
  format = "csv"
1030
1041
 
1031
1042
  # Sort-by parameter
1032
- sortBy = parse_sortby(request.args.get("sortby"))
1043
+ sortBy = parse_collection_sortby(request.args.get("sortby"))
1033
1044
  if not sortBy:
1034
1045
  sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC)])
1035
1046
 
1047
+ if not any(s.field == STAC_FIELD_MAPPINGS["created"] for s in sortBy.fields):
1048
+ sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC))
1049
+ if not any(s.field == STAC_FIELD_MAPPINGS["id"] for s in sortBy.fields):
1050
+ sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC))
1036
1051
  collection_request = CollectionsRequest(sort_by=sortBy, userOwnsAllCollections=userIdMatchesAccount)
1037
1052
 
1038
1053
  # Filter parameter
1039
- collection_request.user_filter = parse_filter(request.args.get("filter"))
1054
+ collection_request.user_filter = parse_collection_filter(request.args.get("filter"))
1040
1055
 
1041
1056
  # Filters added by the pagination
1042
- collection_request.pagination_filter = parse_filter(request.args.get("page"))
1057
+ collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
1043
1058
 
1044
1059
  # Limit parameter
1045
1060
  # if not specified, the default with CSV it 1000. if there are more, the paginated API should be used
@@ -1095,8 +1110,6 @@ def getUserCollection(userId, userIdMatchesAccount=False):
1095
1110
  MAX(LEAST(90, ST_YMax(s.bbox))) AS maxy,
1096
1111
  MIN(s.inserted_at) AS created,
1097
1112
  MAX(s.updated_at) AS updated,
1098
- MIN({order_column}) AS min_order,
1099
- MAX({order_column}) AS max_order,
1100
1113
  ROUND(SUM(ST_Length(s.geom::geography))) / 1000 AS length_km
1101
1114
  FROM sequences s
1102
1115
  WHERE {filter}
@@ -1115,6 +1128,13 @@ def getUserCollection(userId, userIdMatchesAccount=False):
1115
1128
  else:
1116
1129
  raise errors.InvalidAPIUsage(_("No matching sequences found"), 404)
1117
1130
 
1131
+ datasetBounds = get_dataset_bounds(
1132
+ cursor.connection,
1133
+ collection_request.sort_by,
1134
+ additional_filters=SQL(" AND ").join(meta_filter),
1135
+ additional_filters_params={"account": userId},
1136
+ )
1137
+
1118
1138
  collections = get_collections(collection_request)
1119
1139
 
1120
1140
  sequences_links = [
@@ -1164,10 +1184,9 @@ def getUserCollection(userId, userIdMatchesAccount=False):
1164
1184
  pagination_links = sequences.get_pagination_links(
1165
1185
  route="stac_collections.getUserCollection",
1166
1186
  routeArgs={"userId": str(userId), "limit": collection_request.limit},
1167
- field=sortBy.fields[0].field.stac,
1168
- direction=sortBy.fields[0].direction,
1169
- datasetBounds=Bounds(min=meta_collection["min_order"], max=meta_collection["max_order"]),
1170
- dataBounds=collections.query_first_order_bounds,
1187
+ sortBy=sortBy,
1188
+ datasetBounds=datasetBounds,
1189
+ dataBounds=collections.query_bounds,
1171
1190
  additional_filters=additional_filters,
1172
1191
  )
1173
1192
 
@@ -1,15 +1,20 @@
1
1
  import flask
2
2
  from typing import Dict, Any
3
- from flask import jsonify
3
+ from flask import jsonify, current_app
4
4
  from flask_babel import get_locale
5
5
  from geovisio.web.utils import get_api_version
6
+ from geovisio.utils import db
7
+ from psycopg.rows import class_row
8
+ from typing import Optional
9
+ from pydantic import BaseModel, Field, ConfigDict, field_serializer
10
+ import datetime
6
11
 
7
12
  bp = flask.Blueprint("configuration", __name__, url_prefix="/api")
8
13
 
9
14
 
10
15
  @bp.route("/configuration")
11
16
  def configuration():
12
- """Return instance configuration informations
17
+ """Return instance configuration information
13
18
  ---
14
19
  tags:
15
20
  - Metadata
@@ -29,13 +34,14 @@ def configuration():
29
34
  "name": _get_translated(apiSum.name, userLang),
30
35
  "description": _get_translated(apiSum.description, userLang),
31
36
  "geo_coverage": _get_translated(apiSum.geo_coverage, userLang),
32
- "logo": apiSum.logo,
37
+ "logo": str(apiSum.logo),
33
38
  "color": str(apiSum.color),
34
39
  "email": apiSum.email,
35
40
  "auth": _auth_configuration(),
36
41
  "license": _license_configuration(),
37
42
  "version": get_api_version(),
38
43
  "pages": _get_pages(),
44
+ "defaults": _get_default_values(),
39
45
  }
40
46
  )
41
47
 
@@ -53,6 +59,7 @@ def _auth_configuration():
53
59
  return {
54
60
  "enabled": True,
55
61
  "user_profile": {"url": auth.oauth_provider.user_profile_page_url()},
62
+ "registration_is_open": flask.current_app.config["API_REGISTRATION_IS_OPEN"],
56
63
  "enforce_tos_acceptance": flask.current_app.config["API_ENFORCE_TOS_ACCEPTANCE"],
57
64
  }
58
65
 
@@ -66,9 +73,23 @@ def _license_configuration():
66
73
 
67
74
 
68
75
  def _get_pages():
69
- from geovisio.utils import db
70
- from flask import current_app
71
76
 
72
77
  pages = db.fetchall(current_app, "SELECT distinct(name) FROM pages")
73
78
 
74
79
  return [p[0] for p in pages]
80
+
81
+
82
+ class Config(BaseModel):
83
+ collaborative_metadata: Optional[bool]
84
+ split_distance: Optional[int] = Field(validation_alias="default_split_distance")
85
+ split_time: Optional[datetime.timedelta] = Field(validation_alias="default_split_time")
86
+ duplicate_distance: Optional[float] = Field(validation_alias="default_duplicate_distance")
87
+ duplicate_rotation: Optional[int] = Field(validation_alias="default_duplicate_rotation")
88
+
89
+ @field_serializer("split_time")
90
+ def split_time_to_s(self, s: datetime.timedelta, _):
91
+ return s.total_seconds()
92
+
93
+
94
+ def _get_default_values():
95
+ return db.fetchone(current_app, "SELECT * FROM configurations", row_factory=class_row(Config)).model_dump()
geovisio/web/docs.py CHANGED
@@ -1,5 +1,10 @@
1
- from geovisio.web import annotations, collections, items, prepare, users, utils, upload_set, reports, excluded_areas, pages
2
- from geovisio.utils import upload_set as upload_set_utils, reports as reports_utils, excluded_areas as excluded_areas_utils
1
+ from geovisio.web import collections, items, prepare, users, utils, upload_set, reports, excluded_areas, pages, annotations
2
+ from geovisio.utils import (
3
+ upload_set as upload_set_utils,
4
+ reports as reports_utils,
5
+ excluded_areas as excluded_areas_utils,
6
+ annotations as annotations_utils,
7
+ )
3
8
  from importlib import metadata
4
9
  import re
5
10
 
@@ -240,6 +245,12 @@ Note that you may not rely only on these ID that could change through time.
240
245
  "PreparationParameter": prepare.PreparationParameter.model_json_schema(
241
246
  ref_template="#/components/schemas/PreparationParameter/$defs/{model}", mode="serialization"
242
247
  ),
248
+ "GeovisioPostToken": {
249
+ "type": "object",
250
+ "properties": {
251
+ "description": {"type": "string", "description": "optional description of the token"},
252
+ },
253
+ },
243
254
  "GeoVisioPostUploadSet": upload_set.UploadSetCreationParameter.model_json_schema(
244
255
  ref_template="#/components/schemas/GeoVisioPostUploadSet/$defs/{model}", mode="serialization"
245
256
  ),
@@ -258,6 +269,9 @@ Note that you may not rely only on these ID that could change through time.
258
269
  "GeoVisioUploadSetFiles": upload_set_utils.UploadSetFiles.model_json_schema(
259
270
  ref_template="#/components/schemas/GeoVisioUploadSetFiles/$defs/{model}", mode="serialization"
260
271
  ),
272
+ "UploadSetUpdateParameter": upload_set.UploadSetUpdateParameter.model_json_schema(
273
+ ref_template="#/components/schemas/UploadSetUpdateParameter/$defs/{model}", mode="serialization"
274
+ ),
261
275
  "GeoVisioCollectionOfCollection": {
262
276
  "allOf": [
263
277
  {"$ref": "#/components/schemas/STACCollection"},
@@ -295,7 +309,7 @@ Note that you may not rely only on these ID that could change through time.
295
309
  },
296
310
  "GeoVisioCSVCollections": {
297
311
  "type": "string",
298
- "descrition": f"""CSV file containing the collections.
312
+ "description": f"""CSV file containing the collections.
299
313
 
300
314
  The CSV headers will be:
301
315
  * id: ID of the collection
@@ -456,12 +470,32 @@ The CSV headers will be:
456
470
  "datetimetz": {
457
471
  "type": "string",
458
472
  "format": "date-time",
459
- "title": "Date & time with original timezone information",
473
+ "title": "Date & time of the picture (when it was captured).",
474
+ },
475
+ "datetimetz": {
476
+ "type": "string",
477
+ "format": "date-time",
478
+ "title": "Date & time of the picture (when it was captured) with original timezone information",
479
+ },
480
+ "created": {
481
+ "type": "string",
482
+ "format": "date-time",
483
+ "title": "Date & time of picture upload",
484
+ },
485
+ "updated": {
486
+ "type": "string",
487
+ "format": "date-time",
488
+ "title": "Date & time of picture's metadata update",
460
489
  },
461
490
  "geovisio:status": {"$ref": "#/components/schemas/GeoVisioItemStatus"},
462
491
  "geovisio:producer": {"type": "string"},
463
492
  "geovisio:image": {"type": "string", "format": "uri"},
464
493
  "geovisio:thumbnail": {"type": "string", "format": "uri"},
494
+ "geovisio:rank_in_collection": {
495
+ "type": "integer",
496
+ "minimum": 1,
497
+ "title": "Rank of the picture in its collection.",
498
+ },
465
499
  "original_file:size": {"type": "integer", "minimum": 0, "title": "Size of the original file, in bytes"},
466
500
  "original_file:name": {"type": "string", "title": "Original file name"},
467
501
  "panoramax:horizontal_pixel_density": {
@@ -525,7 +559,7 @@ The CSV headers will be:
525
559
  "override_capture_time": {
526
560
  "type": "string",
527
561
  "format": "date-time",
528
- "description": "datetime when the picture was taken. It will change the picture's metadata with this datetime. It should be an iso 3339 formated datetime (like '2017-07-21T17:32:28Z')",
562
+ "description": "datetime when the picture was taken. It will change the picture's metadata with this datetime. It should be an iso 3339 formatted datetime (like '2017-07-21T17:32:28Z')",
529
563
  },
530
564
  "override_latitude": {
531
565
  "type": "number",
@@ -578,6 +612,20 @@ Example values are:
578
612
  Note that this parameter is not taken in account for 360° pictures, as by definition a nearby place would be theorically always visible in it.
579
613
  """,
580
614
  },
615
+ "sortby": {
616
+ "description": """Define the sort order of the results of a search.
617
+ Sort order is defined based on preceding '+' (asc) or '-' (desc).
618
+
619
+ By default we sort to get the last updated pictures first.
620
+
621
+ Available properties are:
622
+ * `ts`: capture datetime of the picture
623
+ * `updated`: sort by updated datetime of the picture
624
+ * `id`: us the picture ID for sort
625
+ """,
626
+ "default": "-updated",
627
+ "type": "string",
628
+ },
581
629
  },
582
630
  },
583
631
  ],
@@ -742,6 +790,10 @@ Available properties are:
742
790
  "properties": {
743
791
  "user_profile": {"type": "object", "properties": {"url": {"type": "string"}}},
744
792
  "enabled": {"type": "boolean"},
793
+ "registration_is_open": {
794
+ "type": "boolean",
795
+ "description": "If true, users can create their own account on the instance. Only used for reference in the federation for the moment",
796
+ },
745
797
  "enforce_tos_acceptance": {"type": "boolean"},
746
798
  },
747
799
  "required": ["enabled"],
@@ -759,6 +811,32 @@ Available properties are:
759
811
  "description": "The GeoVisio API version number",
760
812
  "example": "2.6.0-12-ab12cd34",
761
813
  },
814
+ "defaults": {
815
+ "type": "object",
816
+ "properties": {
817
+ "collaborative_metadata": {
818
+ "type": "integer",
819
+ "description": "If `true`, the pictures's metadata will be, by default, editable by all users.",
820
+ },
821
+ "split_distance": {
822
+ "type": "integer",
823
+ "description": "Maximum distance between two pictures to be considered in the same sequence (in meters). If both split_distance and split_time are None, no split will occur by default.",
824
+ },
825
+ "split_time": {
826
+ "type": "integer",
827
+ "description": "Maximum time interval between two pictures to be considered in the same sequence. If both split_distance and split_time are None, no split will occur by default.",
828
+ },
829
+ "duplicate_distance": {
830
+ "type": "integer",
831
+ "description": "Maximum distance between two pictures to be considered as duplicates (in meters). If both duplicate_distance andduplicate_rotation are None, no deduplication will occur by default.",
832
+ },
833
+ "duplicate_rotation": {
834
+ "type": "integer",
835
+ "description": "Maximum angle of rotation for two too-close-pictures to be considered as duplicates (in degrees).",
836
+ },
837
+ },
838
+ "required": ["collaborative_metadata", "duplicate_distance", "duplicate_rotation", "split_distance", "split_time"],
839
+ },
762
840
  },
763
841
  "required": ["auth"],
764
842
  },
@@ -821,6 +899,15 @@ Available properties are:
821
899
  "payload": {"type": "object", "description": "The error payload"},
822
900
  },
823
901
  },
902
+ "GeoVisioAnnotation": annotations_utils.Annotation.model_json_schema(
903
+ ref_template="#/components/schemas/GeoVisioAnnotation/$defs/{model}", mode="serialization"
904
+ ),
905
+ "GeoVisioPostAnnotation": annotations.AnnotationPostParameter.model_json_schema(
906
+ ref_template="#/components/schemas/GeoVisioPostAnnotation/$defs/{model}", mode="serialization"
907
+ ),
908
+ "GeoVisioPatchAnnotation": annotations.AnnotationPatchParameter.model_json_schema(
909
+ ref_template="#/components/schemas/GeoVisioPatchAnnotation/$defs/{model}", mode="serialization"
910
+ ),
824
911
  },
825
912
  "parameters": {
826
913
  "STAC_bbox": {"$ref": f"https://api.stacspec.org/v{utils.STAC_VERSION}/item-search/openapi.yaml#/components/parameters/bbox"},
@@ -848,7 +935,7 @@ Available properties are:
848
935
  "description": """
849
936
  A CQL2 filter expression for filtering sequences.
850
937
 
851
- Allowed properties are:
938
+ Allowed properties are:
852
939
  * "created": upload date
853
940
  * "updated": last edit date
854
941
  * "status": status of the sequence. Can either be "ready" (for collections ready to be served) or "deleted" for deleted collection. By default, only the "ready" collections will be shown.
@@ -872,7 +959,7 @@ Examples:
872
959
  "description": """
873
960
  A CQL2 filter expression for filtering tiles.
874
961
 
875
- Allowed properties are:
962
+ Allowed properties are:
876
963
  * "status": status of the sequence. Can either be "ready" (for collections ready to be served) or "hidden" for hidden collections. By default, only the "ready" collections will be shown.
877
964
 
878
965
  Usage doc can be found here: https://docs.geoserver.org/2.23.x/en/user/tutorials/cql/cql_tutorial.html
@@ -916,13 +1003,58 @@ Note that this parameter is not taken in account for 360° pictures, as by defin
916
1003
  "required": False,
917
1004
  "schema": {"type": "integer", "minimum": 2, "maximum": 180, "default": 30},
918
1005
  },
1006
+ "GeoVisioSearchSortedBy": {
1007
+ "name": "sortby",
1008
+ "in": "query",
1009
+ "description": """Define the sort order of the results of a search.
1010
+ Sort order is defined based on preceding '+' (asc) or '-' (desc).
1011
+
1012
+ By default we sort to get the last updated pictures firstn (-updated).
1013
+
1014
+ Available properties are:
1015
+ * `ts`: capture datetime of the picture
1016
+ * `updated`: sort by updated datetime of the picture
1017
+ * `id`: us the picture ID for sort
1018
+ """,
1019
+ "required": False,
1020
+ "schema": {
1021
+ "type": "string",
1022
+ },
1023
+ },
1024
+ "searchCQL2_filter": {
1025
+ "name": "filter",
1026
+ "in": "query",
1027
+ "description": """
1028
+ A CQL2 filter expression for filtering search results.
1029
+
1030
+ Only works for semantic search for the moment.
1031
+
1032
+ The attributes must start with "semantics." and formated like "semantics.some_key"='some_value'.
1033
+
1034
+ Note: it's important for the attribute to be quoted (`"`) and the value to around simple quotes (`'`) to avoid issues with CQL2 parsing.
1035
+
1036
+ For the moment only equality (`=`) and list (`IN`) filters are supported. We do not support searching for multiple different tags at once with an `AND` operator (for example, `"semantics.traffic_sign"='yes' AND "semantics.colour"='red'` __will not work__). We suggest to filter data on your side, after retrieving by the main attribute depending on your interest.
1037
+
1038
+ To search for any values of a semantic tag, use `semantics.some_key IS NOT NULL` (case matter here).
1039
+
1040
+ Examples:
1041
+
1042
+ * "semantics.osm|traffic_sign"='yes'
1043
+ * "semantics.osm|traffic_sign" IS NOT NULL'
1044
+ * "semantics.osm|amenity" IN ('bench', 'whatever') OR "semantics.osm|traffic_sign"='yes'
1045
+ """,
1046
+ "required": False,
1047
+ "schema": {
1048
+ "type": "string",
1049
+ },
1050
+ },
919
1051
  "GeoVisioReports_filter": {
920
1052
  "name": "filter",
921
1053
  "in": "query",
922
1054
  "description": """
923
1055
  A CQL2 filter expression for filtering reports.
924
1056
 
925
- Allowed properties are:
1057
+ Allowed properties are:
926
1058
  * status: 'open', 'open_autofix', 'waiting', 'closed_solved', 'closed_ignored'
927
1059
  * reporter: 'me', user account ID or unset
928
1060
  * owner: 'me', user account ID or unset
@@ -947,7 +1079,7 @@ By default, we only show open or waiting reports, sorted by descending creation
947
1079
  "description": """
948
1080
  A CQL2 filter expression for filtering reports.
949
1081
 
950
- Allowed properties are:
1082
+ Allowed properties are:
951
1083
  * status: 'open', 'open_autofix', 'waiting', 'closed_solved', 'closed_ignored'
952
1084
  * reporter: 'me' or unset
953
1085
  * owner: 'me' or unset
@@ -972,7 +1104,7 @@ By default, we only show open or waiting reports concerning you, sorted by desce
972
1104
  "description": """
973
1105
  A CQL2 filter expression for filtering upload sets.
974
1106
 
975
- Allowed properties are:
1107
+ Allowed properties are:
976
1108
  * completed: TRUE or FALSE
977
1109
  * dispatched: TRUE or FALSE
978
1110
 
@@ -1042,7 +1174,7 @@ def getApiInfo():
1042
1174
  """Return API metadata parsed from pyproject.toml"""
1043
1175
  apiMeta = metadata.metadata("geovisio")
1044
1176
 
1045
- # url is formated like 'Home, <url>
1177
+ # url is formatted like 'Home, <url>
1046
1178
  url = apiMeta["Project-URL"].split(",")[1].rstrip()
1047
1179
  # there can be several authors, but we only display the first one in docs
1048
1180
  author = apiMeta["Author-email"].split(",")[0]