geovisio 2.6.0__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. geovisio/__init__.py +36 -7
  2. geovisio/admin_cli/db.py +1 -4
  3. geovisio/config_app.py +40 -1
  4. geovisio/db_migrations.py +24 -3
  5. geovisio/templates/main.html +13 -13
  6. geovisio/templates/viewer.html +3 -3
  7. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  8. geovisio/translations/de/LC_MESSAGES/messages.po +667 -0
  9. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  10. geovisio/translations/en/LC_MESSAGES/messages.po +730 -0
  11. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  12. geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
  13. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  14. geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
  15. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  16. geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
  17. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  18. geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
  19. geovisio/translations/messages.pot +686 -0
  20. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/nl/LC_MESSAGES/messages.po +594 -0
  22. geovisio/utils/__init__.py +1 -1
  23. geovisio/utils/auth.py +50 -11
  24. geovisio/utils/db.py +65 -0
  25. geovisio/utils/excluded_areas.py +83 -0
  26. geovisio/utils/extent.py +30 -0
  27. geovisio/utils/fields.py +1 -1
  28. geovisio/utils/filesystems.py +0 -1
  29. geovisio/utils/link.py +14 -0
  30. geovisio/utils/params.py +20 -0
  31. geovisio/utils/pictures.py +92 -68
  32. geovisio/utils/reports.py +171 -0
  33. geovisio/utils/sequences.py +264 -126
  34. geovisio/utils/tokens.py +37 -42
  35. geovisio/utils/upload_set.py +654 -0
  36. geovisio/web/auth.py +37 -37
  37. geovisio/web/collections.py +286 -302
  38. geovisio/web/configuration.py +14 -0
  39. geovisio/web/docs.py +241 -14
  40. geovisio/web/excluded_areas.py +377 -0
  41. geovisio/web/items.py +156 -108
  42. geovisio/web/map.py +20 -20
  43. geovisio/web/params.py +69 -26
  44. geovisio/web/pictures.py +14 -31
  45. geovisio/web/reports.py +399 -0
  46. geovisio/web/rss.py +13 -7
  47. geovisio/web/stac.py +129 -134
  48. geovisio/web/tokens.py +98 -109
  49. geovisio/web/upload_set.py +768 -0
  50. geovisio/web/users.py +100 -73
  51. geovisio/web/utils.py +28 -9
  52. geovisio/workers/runner_pictures.py +252 -204
  53. {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/METADATA +16 -13
  54. geovisio-2.7.0.dist-info/RECORD +66 -0
  55. geovisio-2.6.0.dist-info/RECORD +0 -41
  56. {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/LICENSE +0 -0
  57. {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/WHEEL +0 -0
geovisio/web/stac.py CHANGED
@@ -1,8 +1,8 @@
1
- import psycopg
2
1
  from psycopg.sql import SQL
3
2
  from flask import Blueprint, current_app, request, url_for
3
+ from flask_babel import gettext as _
4
4
  from geovisio import errors
5
- from geovisio.utils import auth
5
+ from geovisio.utils import auth, db
6
6
  from psycopg.rows import dict_row
7
7
  from geovisio.utils.fields import SortBy, SQLDirection, Bounds, SortByField
8
8
  from geovisio.web.utils import (
@@ -14,6 +14,7 @@ from geovisio.web.utils import (
14
14
  get_root_link,
15
15
  removeNoneInDict,
16
16
  user_dependant_response,
17
+ get_api_version,
17
18
  )
18
19
  from geovisio.utils.sequences import (
19
20
  get_collections,
@@ -57,140 +58,134 @@ def getLanding():
57
58
  $ref: '#/components/schemas/GeoVisioLanding'
58
59
  """
59
60
 
60
- with psycopg.connect(current_app.config["DB_URL"]) as conn:
61
- with conn.cursor() as cursor:
62
- spatial_xmin, spatial_ymin, spatial_xmax, spatial_ymax, temporal_min, temporal_max = cursor.execute(
63
- """
64
- SELECT
65
- GREATEST(-180, ST_XMin(ST_EstimatedExtent('pictures', 'geom'))),
66
- GREATEST(-90, ST_YMin(ST_EstimatedExtent('pictures', 'geom'))),
67
- LEAST(180, ST_XMax(ST_EstimatedExtent('pictures', 'geom'))),
68
- LEAST(90, ST_YMax(ST_EstimatedExtent('pictures', 'geom'))),
69
- MIN(ts), MAX(ts)
70
- FROM pictures
71
- """
72
- ).fetchone()
73
-
74
- extent = (
75
- cleanNoneInDict(
76
- {
77
- "spatial": (
78
- {"bbox": [[spatial_xmin, spatial_ymin, spatial_xmax, spatial_ymax]]} if spatial_xmin is not None else None
79
- ),
80
- "temporal": (
81
- {"interval": [[dbTsToStac(temporal_min), dbTsToStac(temporal_max)]]} if temporal_min is not None else None
82
- ),
83
- }
84
- )
85
- if spatial_xmin is not None or temporal_min is not None
86
- else None
61
+ with db.cursor(current_app) as cursor:
62
+ spatial_xmin, spatial_ymin, spatial_xmax, spatial_ymax, temporal_min, temporal_max = cursor.execute(
63
+ """SELECT
64
+ GREATEST(-180, ST_XMin(ST_EstimatedExtent('pictures', 'geom'))),
65
+ GREATEST(-90, ST_YMin(ST_EstimatedExtent('pictures', 'geom'))),
66
+ LEAST(180, ST_XMax(ST_EstimatedExtent('pictures', 'geom'))),
67
+ LEAST(90, ST_YMax(ST_EstimatedExtent('pictures', 'geom'))),
68
+ MIN(ts), MAX(ts)
69
+ FROM pictures
70
+ """
71
+ ).fetchone()
72
+
73
+ extent = (
74
+ cleanNoneInDict(
75
+ {
76
+ "spatial": ({"bbox": [[spatial_xmin, spatial_ymin, spatial_xmax, spatial_ymax]]} if spatial_xmin is not None else None),
77
+ "temporal": (
78
+ {"interval": [[dbTsToStac(temporal_min), dbTsToStac(temporal_max)]]} if temporal_min is not None else None
79
+ ),
80
+ }
87
81
  )
82
+ if spatial_xmin is not None or temporal_min is not None
83
+ else None
84
+ )
88
85
 
89
- sequences = [
90
- {"rel": "child", "title": f'User "{s[1]}" sequences', "href": url_for("stac.getUserCatalog", userId=s[0], _external=True)}
91
- for s in cursor.execute(
92
- """
93
- SELECT DISTINCT s.account_id, a.name
94
- FROM sequences s
95
- JOIN accounts a ON s.account_id = a.id
96
- """
97
- ).fetchall()
98
- ]
86
+ catalog = dbSequencesToStacCatalog(
87
+ id="geovisio",
88
+ title=current_app.config["API_SUMMARY"].name.get("en"),
89
+ description=current_app.config["API_SUMMARY"].description.get("en"),
90
+ sequences=[],
91
+ request=request,
92
+ extent=extent,
93
+ )
99
94
 
100
- catalog = dbSequencesToStacCatalog(
101
- "geovisio",
102
- "GeoVisio STAC API",
103
- "This catalog list all geolocated pictures available in this GeoVisio instance",
104
- sequences,
105
- request,
106
- extent,
107
- )
95
+ catalog["geovisio_version"] = get_api_version()
108
96
 
109
- mapUrl = (
110
- url_for("map.getTile", x="111", y="222", z="333", format="mvt", _external=True)
111
- .replace("111", "{x}")
112
- .replace("222", "{y}")
113
- .replace("333", "{z}")
114
- )
115
- userMapUrl = (
116
- url_for("map.getUserTile", userId="bob", x="111", y="222", z="333", format="mvt", _external=True)
117
- .replace("111", "{x}")
118
- .replace("222", "{y}")
119
- .replace("333", "{z}")
120
- .replace("bob", "{userId}")
121
- )
122
- userStyleUrl = url_for("map.getUserStyle", userId="bob", _external=True).replace("bob", "{userId}")
123
-
124
- if "stac_extensions" not in catalog:
125
- catalog["stac_extensions"] = []
126
-
127
- catalog["stac_extensions"] += ["https://stac-extensions.github.io/web-map-links/v1.0.0/schema.json"]
128
-
129
- catalog["links"] += cleanNoneInList(
130
- [
131
- {"rel": "service-desc", "type": "application/json", "href": url_for("flasgger.swagger", _external=True)},
132
- {"rel": "service-doc", "type": "text/html", "href": url_for("flasgger.apidocs", _external=True)},
133
- {"rel": "conformance", "type": "application/json", "href": url_for("stac.getConformance", _external=True)},
134
- {"rel": "data", "type": "application/json", "href": url_for("stac_collections.getAllCollections", _external=True)},
135
- {
136
- "rel": "data",
137
- "type": "application/rss+xml",
138
- "href": url_for("stac_collections.getAllCollections", _external=True, format="rss"),
139
- },
140
- {"rel": "search", "type": "application/geo+json", "href": url_for("stac_items.searchItems", _external=True)},
141
- {
142
- "rel": "xyz",
143
- "type": "application/vnd.mapbox-vector-tile",
144
- "href": mapUrl,
145
- "title": "Pictures and sequences vector tiles",
146
- },
147
- {
148
- "rel": "xyz-style",
149
- "type": "application/json",
150
- "href": url_for("map.getStyle", _external=True),
151
- "title": "MapLibre Style JSON",
152
- },
153
- {
154
- "rel": "user-xyz",
155
- "type": "application/vnd.mapbox-vector-tile",
156
- "href": userMapUrl,
157
- "title": "Pictures and sequences vector tiles for a given user",
158
- },
159
- {
160
- "rel": "user-xyz-style",
161
- "type": "application/json",
162
- "href": userStyleUrl,
163
- "title": "MapLibre Style JSON",
164
- },
165
- {
166
- "rel": "collection-preview",
167
- "type": "image/jpeg",
168
- "href": url_for("stac_collections.getCollectionThumbnail", collectionId="{id}", _external=True),
169
- "title": "Thumbnail URL for a given sequence",
170
- },
171
- {
172
- "rel": "item-preview",
173
- "type": "image/jpeg",
174
- "href": url_for("pictures.getPictureThumb", pictureId="{id}", format="jpg", _external=True),
175
- "title": "Thumbnail URL for a given picture",
176
- },
177
- {
178
- "rel": "users",
179
- "type": "application/json",
180
- "href": url_for("user.listUsers", _external=True),
181
- "title": "List of users",
182
- },
183
- {
184
- "rel": "user-search",
185
- "type": "application/json",
186
- "href": url_for("user.searchUser", _external=True),
187
- "title": "Search users",
188
- },
189
- get_license_link(),
190
- ]
191
- )
97
+ mapUrl = (
98
+ url_for("map.getTile", x="11111", y="22222", z="33333", format="mvt", _external=True)
99
+ .replace("11111", "{x}")
100
+ .replace("22222", "{y}")
101
+ .replace("33333", "{z}")
102
+ )
103
+ userMapUrl = (
104
+ url_for("map.getUserTile", userId="bob", x="11111", y="22222", z="33333", format="mvt", _external=True)
105
+ .replace("11111", "{x}")
106
+ .replace("22222", "{y}")
107
+ .replace("33333", "{z}")
108
+ .replace("bob", "{userId}")
109
+ )
110
+ userStyleUrl = url_for("map.getUserStyle", userId="bob", _external=True).replace("bob", "{userId}")
111
+
112
+ if "stac_extensions" not in catalog:
113
+ catalog["stac_extensions"] = []
114
+
115
+ catalog["stac_extensions"] += ["https://stac-extensions.github.io/web-map-links/v1.0.0/schema.json"]
116
+
117
+ catalog["links"] += cleanNoneInList(
118
+ [
119
+ {"rel": "service-desc", "type": "application/json", "href": url_for("flasgger.swagger", _external=True)},
120
+ {"rel": "service-doc", "type": "text/html", "href": url_for("flasgger.apidocs", _external=True)},
121
+ {"rel": "conformance", "type": "application/json", "href": url_for("stac.getConformance", _external=True)},
122
+ {"rel": "data", "type": "application/json", "href": url_for("stac_collections.getAllCollections", _external=True)},
123
+ {"rel": "child", "type": "application/json", "href": url_for("user.listUsers", _external=True)},
124
+ {
125
+ "rel": "data",
126
+ "type": "application/rss+xml",
127
+ "href": url_for("stac_collections.getAllCollections", _external=True, format="rss"),
128
+ },
129
+ {"rel": "search", "type": "application/geo+json", "href": url_for("stac_items.searchItems", _external=True)},
130
+ {
131
+ "rel": "xyz",
132
+ "type": "application/vnd.mapbox-vector-tile",
133
+ "href": mapUrl,
134
+ "title": "Pictures and sequences vector tiles",
135
+ },
136
+ {
137
+ "rel": "xyz-style",
138
+ "type": "application/json",
139
+ "href": url_for("map.getStyle", _external=True),
140
+ "title": "MapLibre Style JSON",
141
+ },
142
+ {
143
+ "rel": "user-xyz",
144
+ "type": "application/vnd.mapbox-vector-tile",
145
+ "href": userMapUrl,
146
+ "title": "Pictures and sequences vector tiles for a given user",
147
+ },
148
+ {
149
+ "rel": "user-xyz-style",
150
+ "type": "application/json",
151
+ "href": userStyleUrl,
152
+ "title": "MapLibre Style JSON",
153
+ },
154
+ {
155
+ "rel": "collection-preview",
156
+ "type": "image/jpeg",
157
+ "href": url_for("stac_collections.getCollectionThumbnail", collectionId="{id}", _external=True),
158
+ "title": "Thumbnail URL for a given sequence",
159
+ },
160
+ {
161
+ "rel": "item-preview",
162
+ "type": "image/jpeg",
163
+ "href": url_for("pictures.getPictureThumb", pictureId="{id}", format="jpg", _external=True),
164
+ "title": "Thumbnail URL for a given picture",
165
+ },
166
+ {
167
+ "rel": "users",
168
+ "type": "application/json",
169
+ "href": url_for("user.listUsers", _external=True),
170
+ "title": "List of users",
171
+ },
172
+ {
173
+ "rel": "user-search",
174
+ "type": "application/json",
175
+ "href": url_for("user.searchUser", _external=True),
176
+ "title": "Search users",
177
+ },
178
+ {
179
+ "rel": "report",
180
+ "type": "application/json",
181
+ "href": url_for("reports.postReport", _external=True),
182
+ "title": "Post feedback/report about picture or sequence",
183
+ },
184
+ get_license_link(),
185
+ ]
186
+ )
192
187
 
193
- return catalog, 200, {"Content-Type": "application/json"}
188
+ return catalog, 200, {"Content-Type": "application/json"}
194
189
 
195
190
 
196
191
  @bp.route("/conformance")
@@ -335,11 +330,11 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
335
330
 
336
331
  userName = None
337
332
  meta_collection = None
338
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn, conn.cursor() as cursor:
333
+ with db.cursor(current_app, row_factory=dict_row) as cursor:
339
334
  userName = cursor.execute("SELECT name FROM accounts WHERE id = %s", [userId]).fetchone()
340
335
 
341
336
  if not userName:
342
- raise errors.InvalidAPIUsage(f"Impossible to find user {userId}")
337
+ raise errors.InvalidAPIUsage(_("Impossible to find user %(u)s", u=userId))
343
338
  userName = userName["name"]
344
339
 
345
340
  meta_collection = cursor.execute(
@@ -349,7 +344,7 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
349
344
 
350
345
  if not meta_collection or meta_collection["min_order"] is None:
351
346
  # No data found, trying to give the most meaningfull error message
352
- raise errors.InvalidAPIUsage(f"No data loaded for user {userId}", 404)
347
+ raise errors.InvalidAPIUsage(_("No data loaded for user %(u)s", u=userId), 404)
353
348
 
354
349
  db_collections = get_collections(collection_request)
355
350
 
geovisio/web/tokens.py CHANGED
@@ -1,13 +1,13 @@
1
1
  import flask
2
2
  from flask import current_app, url_for, request
3
+ from flask_babel import gettext as _
3
4
  from dateutil import tz
4
- import psycopg
5
5
  from psycopg.rows import dict_row
6
6
  from authlib.jose import jwt
7
7
  from authlib.jose.errors import DecodeError
8
8
  import logging
9
9
  import uuid
10
- from geovisio.utils import auth
10
+ from geovisio.utils import auth, db
11
11
  from geovisio import errors, utils
12
12
 
13
13
 
@@ -36,31 +36,29 @@ def list_tokens(account):
36
36
  $ref: '#/components/schemas/GeoVisioTokens'
37
37
  """
38
38
 
39
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
40
- with conn.cursor() as cursor:
41
- records = cursor.execute(
42
- """
43
- SELECT id, description, generated_at FROM tokens WHERE account_id = %(account)s
44
- """,
45
- {"account": account.id},
46
- ).fetchall()
47
-
48
- tokens = [
39
+ records = db.fetchall(
40
+ current_app,
41
+ "SELECT id, description, generated_at FROM tokens WHERE account_id = %(account)s",
42
+ {"account": account.id},
43
+ row_factory=dict_row,
44
+ )
45
+
46
+ tokens = [
47
+ {
48
+ "id": r["id"],
49
+ "description": r["description"],
50
+ "generated_at": r["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
51
+ "links": [
49
52
  {
50
- "id": r["id"],
51
- "description": r["description"],
52
- "generated_at": r["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
53
- "links": [
54
- {
55
- "rel": "self",
56
- "type": "application/json",
57
- "href": url_for("tokens.get_jwt_token", token_id=r["id"], _external=True),
58
- }
59
- ],
53
+ "rel": "self",
54
+ "type": "application/json",
55
+ "href": url_for("tokens.get_jwt_token", token_id=r["id"], _external=True),
60
56
  }
61
- for r in records
62
- ]
63
- return flask.jsonify(tokens)
57
+ ],
58
+ }
59
+ for r in records
60
+ ]
61
+ return flask.jsonify(tokens)
64
62
 
65
63
 
66
64
  @bp.route("/users/me/tokens/<uuid:token_id>", methods=["GET"])
@@ -90,28 +88,26 @@ def get_jwt_token(token_id: uuid.UUID, account: auth.Account):
90
88
  $ref: '#/components/schemas/GeoVisioEncodedToken'
91
89
  """
92
90
 
93
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
94
- with conn.cursor() as cursor:
95
- # check token existence
96
- token = cursor.execute(
97
- """
98
- SELECT id, description, generated_at FROM tokens WHERE account_id = %(account)s AND id = %(token)s
99
- """,
100
- {"account": account.id, "token": token_id},
101
- ).fetchone()
102
-
103
- if not token:
104
- raise errors.InvalidAPIUsage("Impossible to find token", status_code=404)
105
-
106
- jwt_token = _generate_jwt_token(token["id"])
107
- return flask.jsonify(
108
- {
109
- "jwt_token": jwt_token,
110
- "id": token["id"],
111
- "description": token["description"],
112
- "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
113
- }
114
- )
91
+ token = db.fetchone(
92
+ current_app,
93
+ "SELECT id, description, generated_at FROM tokens WHERE account_id = %(account)s AND id = %(token)s",
94
+ {"account": account.id, "token": token_id},
95
+ row_factory=dict_row,
96
+ )
97
+
98
+ # check token existence
99
+ if not token:
100
+ raise errors.InvalidAPIUsage(_("Impossible to find token"), status_code=404)
101
+
102
+ jwt_token = _generate_jwt_token(token["id"])
103
+ return flask.jsonify(
104
+ {
105
+ "jwt_token": jwt_token,
106
+ "id": token["id"],
107
+ "description": token["description"],
108
+ "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
109
+ }
110
+ )
115
111
 
116
112
 
117
113
  @bp.route("/users/me/tokens/<uuid:token_id>", methods=["DELETE"])
@@ -136,22 +132,16 @@ def revoke_token(token_id: uuid.UUID, account: auth.Account):
136
132
  description: The token has been correctly deleted
137
133
  """
138
134
 
139
- with psycopg.connect(current_app.config["DB_URL"]) as conn:
140
- with conn.cursor() as cursor:
141
- # check token existence
142
- res = cursor.execute(
143
- """
144
- DELETE FROM tokens WHERE account_id = %(account)s AND id = %(token)s
145
- """,
146
- {"account": account.id, "token": token_id},
147
- )
148
-
149
- token_deleted = res.rowcount
135
+ with db.execute(
136
+ current_app,
137
+ "DELETE FROM tokens WHERE account_id = %(account)s AND id = %(token)s",
138
+ {"account": account.id, "token": token_id},
139
+ ) as res:
140
+ token_deleted = res.rowcount
150
141
 
151
- if not token_deleted:
152
- raise errors.InvalidAPIUsage("Impossible to find token", status_code=404)
153
- conn.commit()
154
- return flask.jsonify({"message": "token revoked"}), 200
142
+ if not token_deleted:
143
+ raise errors.InvalidAPIUsage(_("Impossible to find token"), status_code=404)
144
+ return flask.jsonify({"message": "token revoked"}), 200
155
145
 
156
146
 
157
147
  @bp.route("/auth/tokens/generate", methods=["POST"])
@@ -180,30 +170,31 @@ def generate_non_associated_token():
180
170
  $ref: '#/components/schemas/JWTokenClaimable'
181
171
  """
182
172
  description = request.args.get("description", "")
183
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
184
- with conn.cursor() as cursor:
185
- token = cursor.execute(
186
- "INSERT INTO tokens (description) VALUES (%(description)s) RETURNING *",
187
- {"description": description},
188
- ).fetchone()
189
- if not token:
190
- raise errors.InternalError("Impossible to generate a new token")
191
-
192
- jwt_token = _generate_jwt_token(token["id"])
193
- token = {
194
- "id": token["id"],
195
- "jwt_token": jwt_token,
196
- "description": token["description"],
197
- "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
198
- "links": [
199
- {
200
- "rel": "claim",
201
- "type": "application/json",
202
- "href": url_for("tokens.claim_non_associated_token", token_id=token["id"], _external=True),
203
- }
204
- ],
173
+
174
+ token = db.fetchone(
175
+ current_app,
176
+ "INSERT INTO tokens (description) VALUES (%(description)s) RETURNING *",
177
+ {"description": description},
178
+ row_factory=dict_row,
179
+ )
180
+ if not token:
181
+ raise errors.InternalError(_("Impossible to generate a new token"))
182
+
183
+ jwt_token = _generate_jwt_token(token["id"])
184
+ token = {
185
+ "id": token["id"],
186
+ "jwt_token": jwt_token,
187
+ "description": token["description"],
188
+ "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
189
+ "links": [
190
+ {
191
+ "rel": "claim",
192
+ "type": "application/json",
193
+ "href": url_for("tokens.claim_non_associated_token", token_id=token["id"], _external=True),
205
194
  }
206
- return flask.jsonify(token)
195
+ ],
196
+ }
197
+ return flask.jsonify(token)
207
198
 
208
199
 
209
200
  @bp.route("/auth/tokens/<uuid:token_id>/claim", methods=["GET"])
@@ -229,30 +220,28 @@ def claim_non_associated_token(token_id, account):
229
220
  200:
230
221
  description: The token has been correctly associated to the account
231
222
  """
232
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
233
- with conn.cursor() as cursor:
234
- token = cursor.execute(
235
- """
236
- SELECT account_id FROM tokens WHERE id = %(token)s
237
- """,
238
- {"token": token_id},
239
- ).fetchone()
240
- if not token:
241
- raise errors.InvalidAPIUsage("Impossible to find token", status_code=404)
242
-
243
- associated_account = token["account_id"]
244
- if associated_account:
245
- if associated_account != account.id:
246
- raise errors.InvalidAPIUsage("Token already claimed by another account", status_code=403)
247
- else:
248
- return flask.jsonify({"message": "token already associated to account"}), 200
249
-
250
- cursor.execute(
251
- "UPDATE tokens SET account_id = %(account)s WHERE id = %(token)s",
252
- {"account": account.id, "token": token_id},
253
- )
254
- conn.commit()
255
- return "You are now logged in the CLI, you can upload your pictures", 200
223
+ with db.cursor(current_app, row_factory=dict_row) as cursor:
224
+ token = cursor.execute(
225
+ """
226
+ SELECT account_id FROM tokens WHERE id = %(token)s
227
+ """,
228
+ {"token": token_id},
229
+ ).fetchone()
230
+ if not token:
231
+ raise errors.InvalidAPIUsage(_("Impossible to find token"), status_code=404)
232
+
233
+ associated_account = token["account_id"]
234
+ if associated_account:
235
+ if associated_account != account.id:
236
+ raise errors.InvalidAPIUsage(_("Token already claimed by another account"), status_code=403)
237
+ else:
238
+ return flask.jsonify({"message": "token already associated to account"}), 200
239
+
240
+ cursor.execute(
241
+ "UPDATE tokens SET account_id = %(account)s WHERE id = %(token)s",
242
+ {"account": account.id, "token": token_id},
243
+ )
244
+ return "You are now logged in the CLI, you can upload your pictures", 200
256
245
 
257
246
 
258
247
  def _generate_jwt_token(token_id: uuid.UUID) -> str: