Coverage for webapp/endpoints/models.py: 94%
255 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 22:43 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 22:43 +0000
1# Packages
2import flask
3from flask import make_response
4from canonicalwebteam.exceptions import (
5 StoreApiResponseErrorList,
6 StoreApiResourceNotFound,
7)
8from canonicalwebteam.store_api.publishergw import PublisherGW
10# Local
11from webapp.decorators import login_required, exchange_required
12from webapp.helpers import api_publisher_session, get_brand_id
14publisher_gateway = PublisherGW("snap", api_publisher_session)
16models = flask.Blueprint(
17 "models",
18 __name__,
19)
22@models.route("/api/store/<store_id>/models")
23@login_required
24@exchange_required
25def get_models(store_id: str):
26 """
27 Retrieves models associated with a given store ID.
29 Args:
30 store_id (int): The ID of the store for which to retrieve models.
32 Returns:
33 dict: A dictionary containing the response message, success status,
34 and data.
35 """
37 res = {}
38 try:
39 models = publisher_gateway.get_store_models(flask.session, store_id)
40 res["success"] = True
41 res["data"] = models
42 response = make_response(res, 200)
43 response.cache_control.max_age = "3600"
44 except StoreApiResponseErrorList as error_list:
45 error_messages = [
46 f"{error.get('message', 'An error occurred')}"
47 for error in error_list.errors
48 ]
49 if "unauthorized" in error_messages:
50 res["message"] = "Store not found"
51 else:
52 res["message"] = " ".join(error_messages)
53 res["success"] = False
54 response = make_response(res, 500)
56 return response
59@models.route("/api/store/<store_id>/models/<model_name>/policies")
60@login_required
61@exchange_required
62def get_policies(store_id: str, model_name: str):
63 """
64 Get the policies for a given store model.
66 Args:
67 store_id (str): The ID of the store.
68 model_name (str): The name of the model.
70 Returns:
71 dict: A dictionary containing the response message and success
72 """
73 brand_id = get_brand_id(flask.session, store_id)
74 res = {}
76 try:
77 policies = publisher_gateway.get_store_model_policies(
78 flask.session, brand_id, model_name
79 )
80 res["success"] = True
81 res["data"] = policies
82 response = make_response(res, 200)
83 response.cache_control.max_age = "3600"
84 return response
85 except StoreApiResponseErrorList as error_list:
86 res["success"] = False
87 res["message"] = " ".join(
88 [
89 f"{error.get('message', 'An error occurred')}"
90 for error in error_list.errors
91 ]
92 )
93 except Exception:
94 res["success"] = False
95 res["message"] = "An error occurred"
97 return make_response(res, 500)
100@models.route(
101 "/api/store/<store_id>/models/<model_name>/policies", methods=["POST"]
102)
103@login_required
104@exchange_required
105def create_policy(store_id: str, model_name: str):
106 """
107 Creat policy for a store model.
109 Args:
110 store_id (str): The ID of the store.
111 model_name (str): The name of the model.
113 Returns:
114 dict: A dictionary containing the response message and success
115 """
116 signing_key = flask.request.form.get("signing_key")
117 res = {}
118 try:
119 signing_keys_data = publisher_gateway.get_store_signing_keys(
120 flask.session, store_id
121 )
122 signing_keys = [key["sha3-384"] for key in signing_keys_data]
124 if not signing_key:
125 res["message"] = "Signing key required"
126 res["success"] = False
127 return make_response(res, 500)
129 if signing_key in signing_keys:
130 publisher_gateway.create_store_model_policy(
131 flask.session, store_id, model_name, signing_key
132 )
133 res["success"] = True
134 else:
135 res["message"] = "Invalid signing key"
136 res["success"] = False
137 except StoreApiResponseErrorList as error_list:
138 res["success"] = False
139 res["message"] = error_list.errors[0]["message"]
141 if res["success"]:
142 return make_response(res, 200)
143 return make_response(res, 500)
146@models.route(
147 "/api/store/<store_id>/models/<model_name>/policies/<revision>",
148 methods=["DELETE"],
149)
150@login_required
151@exchange_required
152def delete_policy(store_id: str, model_name: str, revision: str):
153 res = {}
154 try:
155 response = publisher_gateway.delete_store_model_policy(
156 flask.session, store_id, model_name, revision
157 )
158 if response.status_code == 204:
159 res = {"success": True}
160 return make_response(res, 200)
161 elif response.status_code == 404:
162 res = {"success": False, "message": "Policy not found"}
163 return make_response(res, 404)
164 except StoreApiResponseErrorList as error_list:
165 res["success"] = False
166 res["message"] = error_list.errors[0]["message"]
167 return make_response(res, 500)
170@models.route("/api/store/<store_id>/models", methods=["POST"])
171@login_required
172@exchange_required
173def create_models(store_id: str):
174 """
175 Create a model for a given store.
177 Args:
178 store_id (str): The ID of the store.
180 Returns:
181 dict: A dictionary containing the response message and success
182 status.
183 """
185 # TO DO: Addn validation that name does not exist already
187 res = {}
189 try:
190 name = flask.request.form.get("name")
191 api_key = flask.request.form.get("api_key", "")
193 if len(name) > 128:
194 res["message"] = "Name is too long. Limit 128 characters"
195 res["success"] = False
196 return make_response(res, 500)
198 if api_key and len(api_key) != 50 and not api_key.isalpha():
199 res["message"] = "Invalid API key"
200 res["success"] = False
201 return make_response(res, 500)
203 publisher_gateway.create_store_model(
204 flask.session, store_id, name, api_key
205 )
207 res["success"] = True
208 return make_response(res, 201)
209 except StoreApiResponseErrorList as error_list:
210 res["success"] = False
211 messages = [
212 f"{error.get('message', 'An error occurred')}"
213 for error in error_list.errors
214 ]
215 res["message"] = (" ").join(messages)
217 except Exception:
218 res["success"] = False
219 res["message"] = "An error occurred"
221 return make_response(res, 500)
224@models.route("/api/store/<store_id>/models/<model_name>", methods=["PATCH"])
225@login_required
226@exchange_required
227def update_model(store_id: str, model_name: str):
228 """
229 Update a model for a given store.
231 Args:
232 store_id (str): The ID of the store.
233 model_name (str): The name of the model.
235 Returns:
236 dict: A dictionary containing the response message and success
237 status.
238 """
239 res = {}
241 try:
242 api_key = flask.request.form.get("api_key", "")
244 if len(api_key) != 50 and not api_key.isalpha():
245 res["message"] = "Invalid API key"
246 res["success"] = False
247 return make_response(res, 500)
249 publisher_gateway.update_store_model(
250 flask.session, store_id, model_name, api_key
251 )
252 res["success"] = True
254 except StoreApiResponseErrorList as error_list:
255 res["success"] = False
256 res["message"] = error_list.errors[0]["message"]
258 except StoreApiResourceNotFound:
259 res["success"] = False
260 res["message"] = "Model not found"
261 if res["success"]:
262 return make_response(res, 200)
263 return make_response(res, 500)
266@models.route("/api/store/<store_id>/models/remodel-allowlist")
267@login_required
268@exchange_required
269def get_remodel_allowlist(store_id: str):
270 """
271 Retrieves remodels associated with a given store ID.
273 Args:
274 store_id (int): The ID of the store for which to retrieve remodels.
276 Returns:
277 dict: A dictionary containing the response message, success status,
278 and data.
279 """
281 params = {
282 # rename `cursor` to `page` on our side for clarity
283 "cursor": flask.request.args.get("page"),
284 "from-model": flask.request.args.get("from-model"),
285 "page-size": flask.request.args.get("page-size"),
286 }
288 res = {}
290 try:
291 allowlist = publisher_gateway.get_remodel_allowlist(
292 flask.session, store_id, params
293 )
294 res["success"] = True
295 res["data"] = allowlist
296 response = make_response(res, 200)
297 response.cache_control.max_age = "3600"
298 except StoreApiResponseErrorList as error_list:
299 error_messages = [
300 f"{error.get('message', 'An error occurred')}"
301 for error in error_list.errors
302 ]
303 if "unauthorized" in error_messages:
304 res["message"] = "Store not found"
305 else:
306 res["message"] = " ".join(error_messages)
307 res["success"] = False
308 response = make_response(res, 500)
310 return response
313@models.route(
314 "/api/store/<store_id>/models/remodel-allowlist", methods=["POST"]
315)
316@login_required
317@exchange_required
318def create_remodel_allowlist(store_id: str):
319 """
320 Create a remodel allowlist for a given store.
322 Args:
323 store_id (str): The ID of the store.
325 Returns:
326 dict: A dictionary containing the response message and success
327 status.
328 """
329 res = {}
331 try:
332 allowlist = flask.request.json
333 publisher_gateway.create_remodel_allowlist(
334 flask.session, store_id, allowlist
335 )
337 res["success"] = True
338 return make_response(res, 201)
339 except StoreApiResponseErrorList as error_list:
340 res["success"] = False
341 messages = [
342 f"{error.get('message', 'An error occurred')}"
343 for error in error_list.errors
344 ]
345 res["message"] = " ".join(messages)
346 return make_response(res, error_list.status_code)
348 except StoreApiResourceNotFound:
349 res["success"] = False
350 res["message"] = "Models not found"
351 return make_response(res, 404)
353 except Exception:
354 res["success"] = False
355 res["message"] = "An error occurred"
357 return make_response(res, 500)
360@models.route(
361 "/api/store/<store_id>/models/remodel-allowlist", methods=["PATCH"]
362)
363@login_required
364@exchange_required
365def update_remodel_allowlist(store_id: str):
366 """
367 Update a remodel allowlist for a given store.
369 Args:
370 store_id (str): The ID of the store.
372 Returns:
373 dict: A dictionary containing the response message and success
374 status.
375 """
376 res = {}
378 try:
379 allowlist = flask.request.get_json(silent=True)
380 if allowlist is None:
381 res["success"] = False
382 res["message"] = "Missing or invalid JSON payload"
383 return make_response(res, 400)
385 # Only wrap in list if payload is a dict, not already a list
386 allowlist_param = (
387 [allowlist] if isinstance(allowlist, dict) else allowlist
388 )
389 publisher_gateway.update_remodel_allowlist(
390 flask.session, store_id, allowlist_param
391 )
393 res["success"] = True
394 return make_response(res, 200)
395 except StoreApiResponseErrorList as error_list:
396 res["success"] = False
397 messages = [
398 f"{error.get('message', 'An error occurred')}"
399 for error in error_list.errors
400 ]
401 res["message"] = " ".join(messages)
402 return make_response(res, error_list.status_code)
404 except StoreApiResourceNotFound:
405 res["success"] = False
406 res["message"] = "Remodel allowlist not found"
407 return make_response(res, 404)
409 except Exception:
410 res["success"] = False
411 res["message"] = "An error occurred"
413 return make_response(res, 500)
416@models.route(
417 "/api/store/<store_id>/models/remodel-allowlist", methods=["DELETE"]
418)
419@login_required
420@exchange_required
421def delete_remodel_allowlist(store_id: str):
422 """
423 Delete a remodel allowlist for a given store.
425 Args:
426 store_id (str): The ID of the store.
428 Returns:
429 dict: A dictionary containing the response message and success
430 status.
431 """
432 res = {}
434 try:
435 allowlist = flask.request.get_json(silent=True)
436 if allowlist is None:
437 res["success"] = False
438 res["message"] = "Missing or invalid JSON payload"
439 return make_response(res, 400)
441 # Only wrap in list if payload is a dict, not already a list
442 allowlist_param = (
443 [allowlist] if isinstance(allowlist, dict) else allowlist
444 )
445 publisher_gateway.delete_remodel_allowlist(
446 flask.session, store_id, allowlist_param
447 )
449 res["success"] = True
450 return make_response(res, 200)
451 except StoreApiResponseErrorList as error_list:
452 res["success"] = False
453 messages = [
454 f"{error.get('message', 'An error occurred')}"
455 for error in error_list.errors
456 ]
457 res["message"] = " ".join(messages)
458 return make_response(res, error_list.status_code)
460 except StoreApiResourceNotFound:
461 res["success"] = False
462 res["message"] = "Remodel allowlist not found"
463 return make_response(res, 404)
465 except Exception:
466 res["success"] = False
467 res["message"] = "An error occurred"
469 return make_response(res, 500)
472@models.route("/api/store/<store_id>/models/<model_name>/serial-log")
473@login_required
474@exchange_required
475def get_serial_log(store_id: str, model_name: str):
476 params = {
477 # rename `cursor` to `page` on our side for clarity
478 "cursor": flask.request.args.get("page"),
479 "start_time": flask.request.args.get("start-time"),
480 "end_time": flask.request.args.get("end-time"),
481 "page_size": flask.request.args.get("page-size"),
482 }
484 res = {}
486 try:
487 logs = publisher_gateway.get_store_model_serial_logs(
488 flask.session, store_id, model_name, params
489 )
490 res["data"] = logs
491 res["success"] = True
492 response = make_response(res, 200)
493 except StoreApiResponseErrorList as error_list:
494 error_messages = [
495 f"{error.get('message', 'An error occurred')}"
496 for error in error_list.errors
497 ]
498 if "unauthorized" in error_messages:
499 res["message"] = "Store not found"
500 else:
501 res["message"] = " ".join(error_messages)
502 res["success"] = False
503 response = make_response(res, 500)
505 return response