geovisio 2.9.0__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 (65) hide show
  1. geovisio/__init__.py +6 -1
  2. geovisio/config_app.py +5 -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 +91 -3
  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 +292 -63
  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 +4 -3
  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 +14 -17
  36. geovisio/utils/auth.py +14 -13
  37. geovisio/utils/cql2.py +2 -2
  38. geovisio/utils/fields.py +14 -2
  39. geovisio/utils/items.py +44 -0
  40. geovisio/utils/model_query.py +2 -2
  41. geovisio/utils/pic_shape.py +1 -1
  42. geovisio/utils/pictures.py +111 -18
  43. geovisio/utils/semantics.py +32 -3
  44. geovisio/utils/sentry.py +1 -1
  45. geovisio/utils/sequences.py +51 -34
  46. geovisio/utils/upload_set.py +285 -198
  47. geovisio/utils/website.py +1 -1
  48. geovisio/web/annotations.py +209 -68
  49. geovisio/web/auth.py +1 -1
  50. geovisio/web/collections.py +26 -22
  51. geovisio/web/configuration.py +24 -4
  52. geovisio/web/docs.py +93 -11
  53. geovisio/web/items.py +197 -121
  54. geovisio/web/params.py +44 -31
  55. geovisio/web/pictures.py +34 -0
  56. geovisio/web/tokens.py +49 -1
  57. geovisio/web/upload_set.py +150 -32
  58. geovisio/web/users.py +4 -4
  59. geovisio/web/utils.py +2 -2
  60. geovisio/workers/runner_pictures.py +128 -23
  61. {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/METADATA +13 -13
  62. geovisio-2.10.0.dist-info/RECORD +105 -0
  63. geovisio-2.9.0.dist-info/RECORD +0 -98
  64. {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/WHEEL +0 -0
  65. {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,17 @@
1
1
  from typing import List, Optional
2
2
  from geovisio.utils import auth, db
3
- from geovisio.utils.annotations import AnnotationCreationParameter, creation_annotation, get_annotation, update_annotation
3
+ from geovisio.utils.annotations import (
4
+ AnnotationCreationParameter,
5
+ creation_annotation,
6
+ get_annotation,
7
+ update_annotation,
8
+ InputAnnotationShape,
9
+ )
4
10
  from geovisio.utils.tags import SemanticTagUpdate
5
11
  from geovisio.web.utils import accountIdOrDefault
6
12
  from geovisio.utils.params import validation_error
7
13
  from geovisio import errors
8
- from pydantic import BaseModel, ValidationError
14
+ from pydantic import BaseModel, ValidationError, Field
9
15
  from uuid import UUID
10
16
  from flask import Blueprint, current_app, request, url_for
11
17
  from flask_babel import gettext as _
@@ -14,86 +20,145 @@ from flask_babel import gettext as _
14
20
  bp = Blueprint("annotations", __name__, url_prefix="/api")
15
21
 
16
22
 
17
- @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations", methods=["POST"])
23
+ class AnnotationPostParameter(BaseModel):
24
+ shape: InputAnnotationShape
25
+ """Shape defining the annotation.
26
+ The annotation shape is either a full geojson geometry or only a bounding box (4 floats).
27
+
28
+ The coordinates should be given in pixel, starting from the bottom left of the picture.
29
+
30
+ Note that the API will always output geometry as geojson geometry (thus will transform the bbox into a polygon).
31
+ """
32
+
33
+ semantics: List[SemanticTagUpdate] = Field(default_factory=list)
34
+ """Semantic tags associated to the annotation"""
35
+
36
+
37
+ @bp.route("/pictures/<uuid:itemId>/annotations", methods=["POST"])
18
38
  @auth.login_required()
19
- def postAnnotation(collectionId, itemId, account):
39
+ def postAnnotationNonStacAlias(itemId, account):
20
40
  """Create an annotation on a picture.
21
41
 
22
42
  The geometry can be provided as a bounding box (a list of 4 integers, minx, miny, maxx, maxy) or as a geojson geometry.
23
43
  All coordinates must be in pixel, starting from the top left of the picture.
24
44
 
25
45
  If an annotation already exists on the picture with the same shape, it will be used.
26
- ---
27
- tags:
28
- - Editing
29
- - Semantics
30
- parameters:
31
- - name: collectionId
32
- in: path
33
- description: ID of collection to retrieve
34
- required: true
35
- schema:
36
- type: string
37
- - name: itemId
38
- in: path
39
- description: ID of item to retrieve
40
- required: true
41
- schema:
42
- type: string
43
- requestBody:
46
+
47
+ The is an alias to the `/api/collections/<collectionId>/items/<itemId>/annotations` endpoint (but you don't need to know the collection ID here).
48
+ ---
49
+ tags:
50
+ - Editing
51
+ - Semantics
52
+ parameters:
53
+ - name: itemId
54
+ in: path
55
+ description: ID of item to retrieve
56
+ required: true
57
+ schema:
58
+ type: string
59
+ requestBody:
60
+ content:
61
+ application/json:
62
+ schema:
63
+ $ref: '#/components/schemas/GeoVisioPostAnnotation'
64
+ security:
65
+ - bearerToken: []
66
+ - cookieAuth: []
67
+ responses:
68
+ 200:
69
+ description: the annotation metadata
44
70
  content:
45
71
  application/json:
46
72
  schema:
47
- $ref: '#/components/schemas/GeoVisioPostAnnotation'
48
- security:
49
- - bearerToken: []
50
- - cookieAuth: []
51
- responses:
52
- 200:
53
- description: the annotation metadata
54
- content:
55
- application/json:
56
- schema:
57
- $ref: '#/components/schemas/GeoVisioAnnotation'
73
+ $ref: '#/components/schemas/GeoVisioAnnotation'
58
74
  """
59
75
 
60
76
  account_id = UUID(accountIdOrDefault(account))
61
77
 
62
78
  pic = db.fetchone(
63
79
  current_app,
64
- "SELECT 1 FROM sequences_pictures WHERE seq_id = %(seq)s AND pic_id = %(pic)s",
65
- {"seq": collectionId, "pic": itemId},
80
+ "SELECT 1 FROM pictures WHERE id = %(pic)s",
81
+ {"pic": itemId},
66
82
  )
67
83
  if not pic:
68
84
  raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
69
85
 
70
86
  if request.is_json and request.json is not None:
71
87
  try:
72
- params = AnnotationCreationParameter(**request.json, account_id=account_id, picture_id=itemId)
88
+ post_params = AnnotationPostParameter(**request.json, account_id=account_id, picture_id=itemId)
73
89
  except ValidationError as ve:
74
90
  raise errors.InvalidAPIUsage(_("Impossible to create an annotation"), payload=validation_error(ve))
75
91
  else:
76
92
  raise errors.InvalidAPIUsage(_("Parameter for creating an annotation should be a valid JSON"), status_code=415)
77
93
 
78
- annotation = creation_annotation(params)
79
-
80
- return (
81
- annotation.model_dump_json(exclude_none=True),
82
- 200,
83
- {
84
- "Content-Type": "application/json",
85
- "Access-Control-Expose-Headers": "Location", # Needed for allowing web browsers access Location header
86
- "Location": url_for(
87
- "annotations.getAnnotation", _external=True, annotationId=annotation.id, collectionId=collectionId, itemId=itemId
88
- ),
89
- },
94
+ creation_params = AnnotationCreationParameter(
95
+ account_id=account_id, picture_id=itemId, shape=post_params.shape, semantics=post_params.semantics
90
96
  )
91
97
 
98
+ with db.conn(current_app) as conn:
99
+ annotation = creation_annotation(creation_params, conn)
100
+
101
+ return (
102
+ annotation.model_dump_json(exclude_none=True),
103
+ 200,
104
+ {
105
+ "Content-Type": "application/json",
106
+ "Access-Control-Expose-Headers": "Location", # Needed for allowing web browsers access Location header
107
+ "Location": url_for("annotations.getAnnotationById", _external=True, annotationId=annotation.id),
108
+ },
109
+ )
110
+
111
+
112
+ @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations", methods=["POST"])
113
+ @auth.login_required()
114
+ def postAnnotation(collectionId, itemId, account):
115
+ """Create an annotation on a picture.
116
+
117
+ The geometry can be provided as a bounding box (a list of 4 integers, minx, miny, maxx, maxy) or as a geojson geometry.
118
+ All coordinates must be in pixel, starting from the top left of the picture.
119
+
120
+ If an annotation already exists on the picture with the same shape, it will be used.
121
+ ---
122
+ tags:
123
+ - Editing
124
+ - Semantics
125
+ parameters:
126
+ - name: collectionId
127
+ in: path
128
+ description: ID of collection to retrieve
129
+ required: true
130
+ schema:
131
+ type: string
132
+ - name: itemId
133
+ in: path
134
+ description: ID of item to retrieve
135
+ required: true
136
+ schema:
137
+ type: string
138
+ requestBody:
139
+ content:
140
+ application/json:
141
+ schema:
142
+ $ref: '#/components/schemas/GeoVisioPostAnnotation'
143
+ security:
144
+ - bearerToken: []
145
+ - cookieAuth: []
146
+ responses:
147
+ 200:
148
+ description: the annotation metadata
149
+ content:
150
+ application/json:
151
+ schema:
152
+ $ref: '#/components/schemas/GeoVisioAnnotation'
153
+ """
154
+ return postAnnotationNonStacAlias(itemId=itemId, account=account)
155
+
92
156
 
93
157
  @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["GET"])
94
158
  def getAnnotation(collectionId, itemId, annotationId):
95
159
  """Get an annotation
96
160
 
161
+ Note that this is the same route as `/api/annotations/<uuid:annotationId>` but you need to know the picture's and collection's IDs.
97
162
  ---
98
163
  tags:
99
164
  - Semantics
@@ -103,19 +168,19 @@ def getAnnotation(collectionId, itemId, annotationId):
103
168
  description: ID of collection
104
169
  required: true
105
170
  schema:
106
- type: string
171
+ type: string
107
172
  - name: itemId
108
173
  in: path
109
174
  description: ID of item
110
175
  required: true
111
176
  schema:
112
- type: string
177
+ type: string
113
178
  - name: annotationId
114
179
  in: path
115
180
  description: ID of annotation
116
181
  required: true
117
182
  schema:
118
- type: string
183
+ type: string
119
184
  security:
120
185
  - bearerToken: []
121
186
  - cookieAuth: []
@@ -136,6 +201,41 @@ def getAnnotation(collectionId, itemId, annotationId):
136
201
  return annotation.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
137
202
 
138
203
 
204
+ @bp.route("/annotations/<uuid:annotationId>", methods=["GET"])
205
+ def getAnnotationById(annotationId):
206
+ """Get an annotation.
207
+
208
+ This is the same route as `/api/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>` but you don't need to know the picture's and collection's IDs.
209
+
210
+ ---
211
+ tags:
212
+ - Semantics
213
+ parameters:
214
+ - name: annotationId
215
+ in: path
216
+ description: ID of annotation
217
+ required: true
218
+ schema:
219
+ type: string
220
+ security:
221
+ - bearerToken: []
222
+ - cookieAuth: []
223
+ responses:
224
+ 200:
225
+ description: the annotation metadata
226
+ content:
227
+ application/json:
228
+ schema:
229
+ $ref: '#/components/schemas/GeoVisioAnnotation'
230
+ """
231
+ with db.conn(current_app) as conn:
232
+ annotation = get_annotation(conn, annotationId)
233
+ if not annotation:
234
+ raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
235
+
236
+ return annotation.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
237
+
238
+
139
239
  class AnnotationPatchParameter(BaseModel):
140
240
  """Parameters used to update an annotation"""
141
241
 
@@ -152,34 +252,29 @@ class AnnotationPatchParameter(BaseModel):
152
252
  """
153
253
 
154
254
 
155
- @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["PATCH"])
255
+ @bp.route("/annotations/<uuid:annotationId>", methods=["PATCH"])
156
256
  @auth.login_required()
157
- def patchAnnotation(collectionId, itemId, annotationId, account):
257
+ def patchAnnotationNonStacAlias(annotationId, account):
158
258
  """Patch an annotation
159
259
 
160
260
  Note that if the annotation has no associated tags anymore, it will be deleted.
261
+
262
+ Note that is an alias to the `/api/collections/<collectionId>/items/<itemId>/annotations/<annotationId>` endpoint (but you don't need to know the collection/item ID here).
161
263
  ---
162
264
  tags:
163
265
  - Semantics
164
266
  parameters:
165
- - name: collectionId
166
- in: path
167
- description: ID of collection
168
- required: true
169
- schema:
170
- type: string
171
- - name: itemId
172
- in: path
173
- description: ID of item
174
- required: true
175
- schema:
176
- type: string
177
267
  - name: annotationId
178
268
  in: path
179
269
  description: ID of annotation
180
270
  required: true
181
271
  schema:
182
- type: string
272
+ type: string
273
+ requestBody:
274
+ content:
275
+ application/json:
276
+ schema:
277
+ $ref: '#/components/schemas/GeoVisioPatchAnnotation'
183
278
  security:
184
279
  - bearerToken: []
185
280
  - cookieAuth: []
@@ -204,10 +299,56 @@ def patchAnnotation(collectionId, itemId, annotationId, account):
204
299
  with db.conn(current_app) as conn:
205
300
 
206
301
  annotation = get_annotation(conn, annotationId)
207
- if not annotation or annotation.picture_id != itemId:
208
- raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=itemId), status_code=404)
302
+ if not annotation:
303
+ raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
209
304
 
210
305
  a = update_annotation(annotation, params.semantics, account.id)
211
306
  if a is None:
212
307
  return "", 204
213
308
  return a.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
309
+
310
+
311
+ @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["PATCH"])
312
+ @auth.login_required()
313
+ def patchAnnotation(collectionId, itemId, annotationId, account):
314
+ """Patch an annotation
315
+
316
+ Note that if the annotation has no associated tags anymore, it will be deleted.
317
+
318
+ Note that is the an alias to the `/api/annotations/<annotationId>` endpoint (but you need to know the collection/item ID here).
319
+ ---
320
+ tags:
321
+ - Semantics
322
+ parameters:
323
+ - name: collectionId
324
+ in: path
325
+ description: ID of collection
326
+ required: true
327
+ schema:
328
+ type: string
329
+ - name: itemId
330
+ in: path
331
+ description: ID of item
332
+ required: true
333
+ schema:
334
+ type: string
335
+ - name: annotationId
336
+ in: path
337
+ description: ID of annotation
338
+ required: true
339
+ schema:
340
+ type: string
341
+ security:
342
+ - bearerToken: []
343
+ - cookieAuth: []
344
+ responses:
345
+ 200:
346
+ description: the annotation metadata
347
+ content:
348
+ application/json:
349
+ schema:
350
+ $ref: '#/components/schemas/GeoVisioAnnotation'
351
+ 204:
352
+ description: The annotation was empty, it has been correctly deleted
353
+ """
354
+ return patchAnnotationNonStacAlias(annotationId=annotationId, account=account)
geovisio/web/auth.py CHANGED
@@ -112,7 +112,7 @@ def auth():
112
112
  def logout():
113
113
  """Log out from geovisio
114
114
  * If the OAuth Provider is keycloak, this will redirect to a keycloak confirmation page,
115
- and uppon confirmation keycloak will call post_logout_redirect that will invalidate the session
115
+ and upon confirmation keycloak will call post_logout_redirect that will invalidate the session
116
116
  * If the OAuth Provider is not keycloak, this will invalidate the session
117
117
  ---
118
118
  tags:
@@ -1,6 +1,4 @@
1
- from copy import deepcopy
2
1
  from enum import Enum
3
- from attr import dataclass
4
2
  from geovisio import errors, utils, db
5
3
  from geovisio.utils import auth, sequences
6
4
  from geovisio.utils.params import validation_error
@@ -11,7 +9,7 @@ from geovisio.web.params import (
11
9
  parse_datetime_interval,
12
10
  parse_bbox,
13
11
  parse_collection_filter,
14
- parse_sortby,
12
+ parse_collection_sortby,
15
13
  parse_collections_limit,
16
14
  )
17
15
  from geovisio.utils.sequences import (
@@ -20,7 +18,7 @@ from geovisio.utils.sequences import (
20
18
  get_collections,
21
19
  get_dataset_bounds,
22
20
  )
23
- from geovisio.utils.fields import SortBy, SortByField, SQLDirection, Bounds, BBox
21
+ from geovisio.utils.fields import SortBy, SortByField, SQLDirection, BBox, parse_relative_heading
24
22
  from geovisio.web.rss import dbSequencesToGeoRSS
25
23
  from psycopg.rows import dict_row
26
24
  from psycopg.sql import SQL
@@ -97,7 +95,7 @@ def dbSequenceToStacCollection(dbSeq, description="A sequence of geolocated pict
97
95
  "title": str(dbSeq["name"]),
98
96
  "description": description,
99
97
  "keywords": ["pictures", str(dbSeq["name"])],
100
- "semantics": dbSeq["semantics"] if "semantics" in dbSeq else None,
98
+ "semantics": dbSeq.get("semantics", []),
101
99
  "license": current_app.config["API_PICTURES_LICENSE_SPDX_ID"],
102
100
  "created": dbTsToStac(dbSeq["created"]),
103
101
  "updated": dbTsToStac(dbSeq.get("updated")),
@@ -160,6 +158,16 @@ def dbSequenceToStacCollection(dbSeq, description="A sequence of geolocated pict
160
158
  "href": url_for("stac_collections.getCollection", _external=True, collectionId=dbSeq["id"]),
161
159
  },
162
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
+ ),
163
171
  ]
164
172
  ),
165
173
  }
@@ -247,7 +255,7 @@ def getAllCollections():
247
255
  format = "rss"
248
256
 
249
257
  # Sort-by parameter
250
- sortBy = parse_sortby(request.args.get("sortby"))
258
+ sortBy = parse_collection_sortby(request.args.get("sortby"))
251
259
  if not sortBy:
252
260
  direction = SQLDirection.DESC if format == "rss" else SQLDirection.ASC
253
261
  sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=direction)])
@@ -382,8 +390,7 @@ def getCollection(collectionId):
382
390
 
383
391
  record = db.fetchone(
384
392
  current_app,
385
- """
386
- SELECT
393
+ """SELECT
387
394
  s.id,
388
395
  s.metadata->>'title' AS name,
389
396
  ST_XMin(s.bbox) AS minx,
@@ -393,6 +400,7 @@ def getCollection(collectionId):
393
400
  s.status AS status,
394
401
  accounts.name AS account_name,
395
402
  s.account_id AS account_id,
403
+ s.upload_set_id,
396
404
  s.inserted_at AS created,
397
405
  s.updated_at AS updated,
398
406
  s.current_sort AS current_sort,
@@ -404,7 +412,7 @@ def getCollection(collectionId):
404
412
  ROUND(ST_Length(s.geom::geography)) / 1000 as length_km,
405
413
  s.computed_h_pixel_density,
406
414
  s.computed_gps_accuracy,
407
- t.semantics
415
+ COALESCE(seq_sem.semantics, '[]'::json) AS semantics
408
416
  FROM sequences s
409
417
  LEFT JOIN (
410
418
  SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
@@ -413,7 +421,7 @@ def getCollection(collectionId):
413
421
  )) ORDER BY key, value) AS semantics
414
422
  FROM sequences_semantics
415
423
  GROUP BY sequence_id
416
- ) t ON t.sequence_id = s.id
424
+ ) seq_sem ON seq_sem.sequence_id = s.id
417
425
  JOIN accounts ON s.account_id = accounts.id, (
418
426
  SELECT
419
427
  array_agg(DISTINCT jsonb_build_object(
@@ -502,6 +510,10 @@ def getCollectionThumbnail(collectionId):
502
510
  @auth.login_required_by_setting("API_FORCE_AUTH_ON_UPLOAD")
503
511
  def postCollection(account=None):
504
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, ...
505
517
  ---
506
518
  tags:
507
519
  - Upload
@@ -511,7 +523,7 @@ def postCollection(account=None):
511
523
  required: false
512
524
  schema:
513
525
  type: string
514
- 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"
515
527
  requestBody:
516
528
  content:
517
529
  application/json:
@@ -565,7 +577,7 @@ class PatchCollectionParameter(BaseModel):
565
577
  """Parameters used to add an item to an UploadSet"""
566
578
 
567
579
  relative_heading: Optional[int] = None
568
- """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."""
569
581
  visible: Optional[bool] = None
570
582
  """Should the sequence be publicly visible ?"""
571
583
  title: Optional[str] = Field(max_length=250, default=None)
@@ -610,15 +622,7 @@ If unset, sort order is unchanged."""
610
622
  @field_validator("relative_heading", mode="before")
611
623
  @classmethod
612
624
  def parse_relative_heading(cls, value):
613
- try:
614
- relHeading = int(value)
615
- if relHeading < -180 or relHeading > 180:
616
- raise ValueError()
617
- return relHeading
618
- except (ValueError, TypeError):
619
- raise errors.InvalidAPIUsage(
620
- _("Relative heading is not valid, should be an integer in degrees from -180 to 180"), status_code=400
621
- )
625
+ return parse_relative_heading(value)
622
626
 
623
627
  def has_only_semantics_updates(self):
624
628
  return self.model_fields_set == {"semantics"}
@@ -1036,7 +1040,7 @@ def getUserCollection(userId, userIdMatchesAccount=False):
1036
1040
  format = "csv"
1037
1041
 
1038
1042
  # Sort-by parameter
1039
- sortBy = parse_sortby(request.args.get("sortby"))
1043
+ sortBy = parse_collection_sortby(request.args.get("sortby"))
1040
1044
  if not sortBy:
1041
1045
  sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC)])
1042
1046
 
@@ -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
@@ -36,6 +41,7 @@ def 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
 
@@ -67,9 +73,23 @@ def _license_configuration():
67
73
 
68
74
 
69
75
  def _get_pages():
70
- from geovisio.utils import db
71
- from flask import current_app
72
76
 
73
77
  pages = db.fetchall(current_app, "SELECT distinct(name) FROM pages")
74
78
 
75
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()