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.
- geovisio/__init__.py +6 -1
- geovisio/config_app.py +16 -5
- geovisio/translations/ar/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ar/LC_MESSAGES/messages.po +818 -0
- geovisio/translations/br/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +55 -2
- geovisio/translations/el/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +193 -139
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +53 -4
- geovisio/translations/es/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/fi/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +101 -6
- geovisio/translations/hu/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +63 -3
- geovisio/translations/ja/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/ko/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/messages.pot +185 -129
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +421 -86
- geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/oc/LC_MESSAGES/messages.po +818 -0
- geovisio/translations/pl/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/sv/LC_MESSAGES/messages.po +823 -0
- geovisio/translations/ti/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ti/LC_MESSAGES/messages.po +762 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +1 -1
- geovisio/utils/annotations.py +183 -0
- geovisio/utils/auth.py +14 -13
- geovisio/utils/cql2.py +134 -0
- geovisio/utils/db.py +7 -0
- geovisio/utils/fields.py +38 -9
- geovisio/utils/items.py +44 -0
- geovisio/utils/model_query.py +4 -4
- geovisio/utils/pic_shape.py +63 -0
- geovisio/utils/pictures.py +164 -29
- geovisio/utils/reports.py +10 -17
- geovisio/utils/semantics.py +196 -57
- geovisio/utils/sentry.py +1 -2
- geovisio/utils/sequences.py +191 -93
- geovisio/utils/tags.py +31 -0
- geovisio/utils/upload_set.py +287 -209
- geovisio/utils/website.py +1 -1
- geovisio/web/annotations.py +346 -9
- geovisio/web/auth.py +1 -1
- geovisio/web/collections.py +73 -54
- geovisio/web/configuration.py +26 -5
- geovisio/web/docs.py +143 -11
- geovisio/web/items.py +232 -155
- geovisio/web/map.py +25 -13
- geovisio/web/params.py +55 -52
- geovisio/web/pictures.py +34 -0
- geovisio/web/stac.py +19 -12
- geovisio/web/tokens.py +49 -1
- geovisio/web/upload_set.py +148 -37
- geovisio/web/users.py +4 -4
- geovisio/web/utils.py +2 -2
- geovisio/workers/runner_pictures.py +190 -24
- {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/METADATA +27 -26
- geovisio-2.10.0.dist-info/RECORD +105 -0
- {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/WHEEL +1 -1
- geovisio-2.8.1.dist-info/RECORD +0 -92
- {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/licenses/LICENSE +0 -0
geovisio/utils/website.py
CHANGED
|
@@ -14,7 +14,7 @@ class Website:
|
|
|
14
14
|
"""Website associated to the API.
|
|
15
15
|
This wrapper will define the routes we expect from the website.
|
|
16
16
|
|
|
17
|
-
We should limit the
|
|
17
|
+
We should limit the interaction from the api to the website, but for some flow (especially auth flows), it's can be useful to redirect to website's page
|
|
18
18
|
|
|
19
19
|
If the url is:
|
|
20
20
|
* set to `false`, there is no associated website
|
geovisio/web/annotations.py
CHANGED
|
@@ -1,17 +1,354 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from geovisio.utils import auth, db
|
|
3
|
+
from geovisio.utils.annotations import (
|
|
4
|
+
AnnotationCreationParameter,
|
|
5
|
+
creation_annotation,
|
|
6
|
+
get_annotation,
|
|
7
|
+
update_annotation,
|
|
8
|
+
InputAnnotationShape,
|
|
9
|
+
)
|
|
10
|
+
from geovisio.utils.tags import SemanticTagUpdate
|
|
5
11
|
from geovisio.web.utils import accountIdOrDefault
|
|
6
|
-
from psycopg.types.json import Jsonb
|
|
7
|
-
from geovisio.utils import db
|
|
8
12
|
from geovisio.utils.params import validation_error
|
|
9
13
|
from geovisio import errors
|
|
10
|
-
from pydantic import BaseModel,
|
|
14
|
+
from pydantic import BaseModel, ValidationError, Field
|
|
11
15
|
from uuid import UUID
|
|
12
|
-
from
|
|
13
|
-
from flask import Blueprint, request, current_app
|
|
16
|
+
from flask import Blueprint, current_app, request, url_for
|
|
14
17
|
from flask_babel import gettext as _
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
bp = Blueprint("annotations", __name__, url_prefix="/api")
|
|
21
|
+
|
|
22
|
+
|
|
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"])
|
|
38
|
+
@auth.login_required()
|
|
39
|
+
def postAnnotationNonStacAlias(itemId, account):
|
|
40
|
+
"""Create an annotation on a picture.
|
|
41
|
+
|
|
42
|
+
The geometry can be provided as a bounding box (a list of 4 integers, minx, miny, maxx, maxy) or as a geojson geometry.
|
|
43
|
+
All coordinates must be in pixel, starting from the top left of the picture.
|
|
44
|
+
|
|
45
|
+
If an annotation already exists on the picture with the same shape, it will be used.
|
|
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
|
|
70
|
+
content:
|
|
71
|
+
application/json:
|
|
72
|
+
schema:
|
|
73
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
account_id = UUID(accountIdOrDefault(account))
|
|
77
|
+
|
|
78
|
+
pic = db.fetchone(
|
|
79
|
+
current_app,
|
|
80
|
+
"SELECT 1 FROM pictures WHERE id = %(pic)s",
|
|
81
|
+
{"pic": itemId},
|
|
82
|
+
)
|
|
83
|
+
if not pic:
|
|
84
|
+
raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
|
|
85
|
+
|
|
86
|
+
if request.is_json and request.json is not None:
|
|
87
|
+
try:
|
|
88
|
+
post_params = AnnotationPostParameter(**request.json, account_id=account_id, picture_id=itemId)
|
|
89
|
+
except ValidationError as ve:
|
|
90
|
+
raise errors.InvalidAPIUsage(_("Impossible to create an annotation"), payload=validation_error(ve))
|
|
91
|
+
else:
|
|
92
|
+
raise errors.InvalidAPIUsage(_("Parameter for creating an annotation should be a valid JSON"), status_code=415)
|
|
93
|
+
|
|
94
|
+
creation_params = AnnotationCreationParameter(
|
|
95
|
+
account_id=account_id, picture_id=itemId, shape=post_params.shape, semantics=post_params.semantics
|
|
96
|
+
)
|
|
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
|
+
|
|
156
|
+
|
|
157
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["GET"])
|
|
158
|
+
def getAnnotation(collectionId, itemId, annotationId):
|
|
159
|
+
"""Get an annotation
|
|
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.
|
|
162
|
+
---
|
|
163
|
+
tags:
|
|
164
|
+
- Semantics
|
|
165
|
+
parameters:
|
|
166
|
+
- name: collectionId
|
|
167
|
+
in: path
|
|
168
|
+
description: ID of collection
|
|
169
|
+
required: true
|
|
170
|
+
schema:
|
|
171
|
+
type: string
|
|
172
|
+
- name: itemId
|
|
173
|
+
in: path
|
|
174
|
+
description: ID of item
|
|
175
|
+
required: true
|
|
176
|
+
schema:
|
|
177
|
+
type: string
|
|
178
|
+
- name: annotationId
|
|
179
|
+
in: path
|
|
180
|
+
description: ID of annotation
|
|
181
|
+
required: true
|
|
182
|
+
schema:
|
|
183
|
+
type: string
|
|
184
|
+
security:
|
|
185
|
+
- bearerToken: []
|
|
186
|
+
- cookieAuth: []
|
|
187
|
+
responses:
|
|
188
|
+
200:
|
|
189
|
+
description: the annotation metadata
|
|
190
|
+
content:
|
|
191
|
+
application/json:
|
|
192
|
+
schema:
|
|
193
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
194
|
+
"""
|
|
195
|
+
with db.conn(current_app) as conn:
|
|
196
|
+
|
|
197
|
+
annotation = get_annotation(conn, annotationId)
|
|
198
|
+
if not annotation or annotation.picture_id != itemId:
|
|
199
|
+
raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=itemId), status_code=404)
|
|
200
|
+
|
|
201
|
+
return annotation.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
|
|
202
|
+
|
|
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
|
+
|
|
239
|
+
class AnnotationPatchParameter(BaseModel):
|
|
240
|
+
"""Parameters used to update an annotation"""
|
|
241
|
+
|
|
242
|
+
semantics: Optional[List[SemanticTagUpdate]] = None
|
|
243
|
+
"""Tags to update on the annotation. By default each tag will be added to the annotation's tags, but you can change this behavior by setting the `action` parameter to `delete`.
|
|
244
|
+
|
|
245
|
+
If you want to replace a tag, you need to first delete it, then add it again.
|
|
246
|
+
|
|
247
|
+
Like:
|
|
248
|
+
[
|
|
249
|
+
{"key": "some_key", "value": "some_value", "action": "delete"},
|
|
250
|
+
{"key": "some_key", "value": "some_new_value"}
|
|
251
|
+
]
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@bp.route("/annotations/<uuid:annotationId>", methods=["PATCH"])
|
|
256
|
+
@auth.login_required()
|
|
257
|
+
def patchAnnotationNonStacAlias(annotationId, account):
|
|
258
|
+
"""Patch an annotation
|
|
259
|
+
|
|
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).
|
|
263
|
+
---
|
|
264
|
+
tags:
|
|
265
|
+
- Semantics
|
|
266
|
+
parameters:
|
|
267
|
+
- name: annotationId
|
|
268
|
+
in: path
|
|
269
|
+
description: ID of annotation
|
|
270
|
+
required: true
|
|
271
|
+
schema:
|
|
272
|
+
type: string
|
|
273
|
+
requestBody:
|
|
274
|
+
content:
|
|
275
|
+
application/json:
|
|
276
|
+
schema:
|
|
277
|
+
$ref: '#/components/schemas/GeoVisioPatchAnnotation'
|
|
278
|
+
security:
|
|
279
|
+
- bearerToken: []
|
|
280
|
+
- cookieAuth: []
|
|
281
|
+
responses:
|
|
282
|
+
200:
|
|
283
|
+
description: the annotation metadata
|
|
284
|
+
content:
|
|
285
|
+
application/json:
|
|
286
|
+
schema:
|
|
287
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
288
|
+
204:
|
|
289
|
+
description: The annotation was empty, it has been correctly deleted
|
|
290
|
+
"""
|
|
291
|
+
if request.is_json and request.json is not None:
|
|
292
|
+
try:
|
|
293
|
+
params = AnnotationPatchParameter(**request.json)
|
|
294
|
+
except ValidationError as ve:
|
|
295
|
+
raise errors.InvalidAPIUsage(_("Impossible to patch annotation, invalid parameters"), payload=validation_error(ve))
|
|
296
|
+
else:
|
|
297
|
+
raise errors.InvalidAPIUsage(_("Parameter for updating an annotation should be a valid JSON"), status_code=415)
|
|
298
|
+
|
|
299
|
+
with db.conn(current_app) as conn:
|
|
300
|
+
|
|
301
|
+
annotation = get_annotation(conn, annotationId)
|
|
302
|
+
if not annotation:
|
|
303
|
+
raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
|
|
304
|
+
|
|
305
|
+
a = update_annotation(annotation, params.semantics, account.id)
|
|
306
|
+
if a is None:
|
|
307
|
+
return "", 204
|
|
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
|
|
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:
|