|
5 | 5 | import inspect
|
6 | 6 | from functools import wraps
|
7 | 7 | from werkzeug.datastructures import ImmutableMultiDict, MultiDict
|
8 |
| -from werkzeug.exceptions import HTTPException, NotFound, GatewayTimeout |
9 | 8 | from sqlalchemy.orm.attributes import InstrumentedAttribute
|
10 |
| -from flask_restx import Api, Namespace, Resource |
11 |
| -from coprs import app |
| 9 | +from flask_restx import Api, Namespace |
12 | 10 | from coprs.exceptions import (
|
13 | 11 | AccessRestricted,
|
14 |
| - ActionInProgressException, |
15 | 12 | CoprHttpException,
|
16 |
| - InsufficientStorage, |
17 |
| - ObjectNotFound, |
18 | 13 | BadRequest,
|
19 | 14 | )
|
20 | 15 | from coprs.logic.complex_logic import ComplexLogic
|
@@ -51,48 +46,68 @@ def home():
|
51 | 46 | # HTTP methods
|
52 | 47 | GET = ["GET"]
|
53 | 48 | POST = ["POST"]
|
| 49 | +# TODO: POST != PUT nor DELETE, we should use at least use these methods according |
| 50 | +# conventions -> POST to create new element, PUT to update element, DELETE to delete |
| 51 | +# https://www.ibm.com/docs/en/urbancode-release/6.1.1?topic=reference-rest-api-conventions |
| 52 | +# fix python-copr firstly please, then put warning header to deprecated methods |
54 | 53 | PUT = ["POST", "PUT"]
|
55 | 54 | DELETE = ["POST", "DELETE"]
|
56 | 55 |
|
57 | 56 |
|
| 57 | +def _convert_query_params(endpoint_method, params_to_not_look_for, **kwargs): |
| 58 | + sig = inspect.signature(endpoint_method) |
| 59 | + params = list(set(sig.parameters) - params_to_not_look_for) |
| 60 | + for arg in params: |
| 61 | + if arg not in flask.request.args: |
| 62 | + # If parameter is present in the URL path, we can use its |
| 63 | + # value instead of failing that it is missing in query |
| 64 | + # parameters, e.g. let's have a view decorated with these |
| 65 | + # two routes: |
| 66 | + # @foo_ns.route("/foo/bar/<int:build>/<chroot>") |
| 67 | + # @foo_ns.route("/foo/bar") accepting ?build=X&chroot=Y |
| 68 | + # @query_params() |
| 69 | + # Then we need the following condition to get the first |
| 70 | + # route working |
| 71 | + if arg in flask.request.view_args: |
| 72 | + continue |
| 73 | + |
| 74 | + # If parameter has a default value, it is not required |
| 75 | + default_parameter_value = sig.parameters[arg].default |
| 76 | + if default_parameter_value != sig.parameters[arg].empty: |
| 77 | + kwargs[arg] = default_parameter_value |
| 78 | + continue |
| 79 | + |
| 80 | + raise BadRequest("Missing argument {}".format(arg)) |
| 81 | + |
| 82 | + kwargs[arg] = flask.request.args.get(arg) |
| 83 | + return kwargs |
| 84 | + |
| 85 | + |
58 | 86 | def query_params():
|
| 87 | + params_to_not_look_for = {"args", "kwargs"} |
| 88 | + |
59 | 89 | def query_params_decorator(f):
|
60 | 90 | @wraps(f)
|
61 | 91 | def query_params_wrapper(*args, **kwargs):
|
62 |
| - sig = inspect.signature(f) |
63 |
| - params = [x for x in sig.parameters] |
64 |
| - params = list(set(params) - {"args", "kwargs"}) |
65 |
| - for arg in params: |
66 |
| - if arg not in flask.request.args: |
67 |
| - # If parameter is present in the URL path, we can use its |
68 |
| - # value instead of failing that it is missing in query |
69 |
| - # parameters, e.g. let's have a view decorated with these |
70 |
| - # two routes: |
71 |
| - # @foo_ns.route("/foo/bar/<int:build>/<chroot>") |
72 |
| - # @foo_ns.route("/foo/bar") accepting ?build=X&chroot=Y |
73 |
| - # @query_params() |
74 |
| - # Then we need the following condition to get the first |
75 |
| - # route working |
76 |
| - if arg in flask.request.view_args: |
77 |
| - continue |
78 |
| - |
79 |
| - # If parameter has a default value, it is not required |
80 |
| - if sig.parameters[arg].default == sig.parameters[arg].empty: |
81 |
| - raise BadRequest("Missing argument {}".format(arg)) |
82 |
| - kwargs[arg] = flask.request.args.get(arg) |
| 92 | + kwargs = _convert_query_params(f, params_to_not_look_for, **kwargs) |
83 | 93 | return f(*args, **kwargs)
|
84 | 94 | return query_params_wrapper
|
85 | 95 | return query_params_decorator
|
86 | 96 |
|
87 | 97 |
|
| 98 | +def _shared_pagination_wrapper(**kwargs): |
| 99 | + form = PaginationForm(flask.request.args) |
| 100 | + if not form.validate(): |
| 101 | + raise CoprHttpException(form.errors) |
| 102 | + kwargs.update(form.data) |
| 103 | + return kwargs |
| 104 | + |
| 105 | + |
88 | 106 | def pagination():
|
89 | 107 | def pagination_decorator(f):
|
90 | 108 | @wraps(f)
|
91 | 109 | def pagination_wrapper(*args, **kwargs):
|
92 |
| - form = PaginationForm(flask.request.args) |
93 |
| - if not form.validate(): |
94 |
| - raise CoprHttpException(form.errors) |
95 |
| - kwargs.update(form.data) |
| 110 | + kwargs = _shared_pagination_wrapper(**kwargs) |
96 | 111 | return f(*args, **kwargs)
|
97 | 112 | return pagination_wrapper
|
98 | 113 | return pagination_decorator
|
@@ -232,19 +247,24 @@ def get(self):
|
232 | 247 | return objects[self.offset : limit]
|
233 | 248 |
|
234 | 249 |
|
| 250 | +def _check_if_user_can_edit_copr(ownername, projectname): |
| 251 | + copr = get_copr(ownername, projectname) |
| 252 | + if not flask.g.user.can_edit(copr): |
| 253 | + raise AccessRestricted( |
| 254 | + "User '{0}' can not see permissions for project '{1}' " \ |
| 255 | + "(missing admin rights)".format( |
| 256 | + flask.g.user.name, |
| 257 | + '/'.join([ownername, projectname]) |
| 258 | + ) |
| 259 | + ) |
| 260 | + return copr |
| 261 | + |
| 262 | + |
235 | 263 | def editable_copr(f):
|
236 | 264 | @wraps(f)
|
237 |
| - def wrapper(ownername, projectname, **kwargs): |
238 |
| - copr = get_copr(ownername, projectname) |
239 |
| - if not flask.g.user.can_edit(copr): |
240 |
| - raise AccessRestricted( |
241 |
| - "User '{0}' can not see permissions for project '{1}' "\ |
242 |
| - "(missing admin rights)".format( |
243 |
| - flask.g.user.name, |
244 |
| - '/'.join([ownername, projectname]) |
245 |
| - ) |
246 |
| - ) |
247 |
| - return f(copr, **kwargs) |
| 265 | + def wrapper(ownername, projectname): |
| 266 | + copr = _check_if_user_can_edit_copr(ownername, projectname) |
| 267 | + return f(copr) |
248 | 268 | return wrapper
|
249 | 269 |
|
250 | 270 |
|
@@ -374,3 +394,109 @@ def rename_fields_helper(input_dict, replace):
|
374 | 394 | for value in values:
|
375 | 395 | output.add(new_key, value)
|
376 | 396 | return output
|
| 397 | + |
| 398 | + |
| 399 | +# Flask-restx specific helpers/decorators - don't use them with regular Flask API! |
| 400 | +# TODO: delete/unify decorators for regular Flask and Flask-restx API once migration |
| 401 | +# is done |
| 402 | + |
| 403 | + |
| 404 | +def query_to_parameters(endpoint_method): |
| 405 | + """ |
| 406 | + Decorator passing query parameters to http method parameters |
| 407 | +
|
| 408 | + Returns: |
| 409 | + Endpoint that has its query parameters can be used as parameters in http method |
| 410 | + """ |
| 411 | + params_to_not_look_for = {"self", "args", "kwargs"} |
| 412 | + |
| 413 | + @wraps(endpoint_method) |
| 414 | + def convert_query_parameters_of_endpoint_method(self, *args, **kwargs): |
| 415 | + kwargs = _convert_query_params(endpoint_method, params_to_not_look_for, **kwargs) |
| 416 | + return endpoint_method(self, *args, **kwargs) |
| 417 | + return convert_query_parameters_of_endpoint_method |
| 418 | + |
| 419 | + |
| 420 | +def deprecated_route_method(ns: Namespace, msg): |
| 421 | + """ |
| 422 | + Decorator that display a deprecation warning in headers and docs. |
| 423 | +
|
| 424 | + Usage: |
| 425 | + class Endpoint(Resource): |
| 426 | + ... |
| 427 | + @deprecated_route_method(foo_ns, "Message e.g. what to use instead") |
| 428 | + ... |
| 429 | + def get(): |
| 430 | + return {"scary": "BOO!"} |
| 431 | +
|
| 432 | + Args: |
| 433 | + ns: flask-restx Namespace |
| 434 | + msg: Deprecation warning message. |
| 435 | + """ |
| 436 | + def decorate_endpoint_method(endpoint_method): |
| 437 | + # render deprecation in API docs |
| 438 | + ns.deprecated(endpoint_method) |
| 439 | + |
| 440 | + @wraps(endpoint_method) |
| 441 | + def warn_user_in_headers(self, *args, **kwargs): |
| 442 | + custom_header = {"Warning": f"This method is deprecated: {msg}"} |
| 443 | + resp = endpoint_method(self, *args, **kwargs) |
| 444 | + if not isinstance(resp, tuple): |
| 445 | + # only resp body as dict was passed |
| 446 | + return resp, custom_header |
| 447 | + |
| 448 | + for part_of_resp in resp[1:]: |
| 449 | + if isinstance(part_of_resp, dict): |
| 450 | + part_of_resp |= custom_header |
| 451 | + return resp |
| 452 | + |
| 453 | + return resp + (custom_header,) |
| 454 | + |
| 455 | + return warn_user_in_headers |
| 456 | + return decorate_endpoint_method |
| 457 | + |
| 458 | + |
| 459 | +def deprecated_route_method_type(ns: Namespace, deprecated_method_type: str, use_instead: str): |
| 460 | + """ |
| 461 | + Calls deprecated_route decorator with specific message about deprecated method. |
| 462 | +
|
| 463 | + Usage: |
| 464 | + class Endpoint(Resource): |
| 465 | + ... |
| 466 | + @deprecated_route_method_type(foo_ns, "POST", "PUT") |
| 467 | + ... |
| 468 | + def get(): |
| 469 | + return {"scary": "BOO!"} |
| 470 | +
|
| 471 | + Args: |
| 472 | + ns: flask-restx Namespace |
| 473 | + deprecated_method_type: method enum e.g. POST |
| 474 | + use_instead: method user should use instead |
| 475 | + """ |
| 476 | + def call_deprecated_endpoint_method(endpoint_method): |
| 477 | + msg = f"Use {use_instead} method instead of {deprecated_method_type}" |
| 478 | + return deprecated_route_method(ns, msg)(endpoint_method) |
| 479 | + return call_deprecated_endpoint_method |
| 480 | + |
| 481 | + |
| 482 | +def restx_editable_copr(endpoint_method): |
| 483 | + """ |
| 484 | + Raises an exception if user don't have permissions for editing Copr repo. |
| 485 | + """ |
| 486 | + @wraps(endpoint_method) |
| 487 | + def editable_copr_getter(self, ownername, projectname): |
| 488 | + copr = _check_if_user_can_edit_copr(ownername, projectname) |
| 489 | + return endpoint_method(self, copr) |
| 490 | + return editable_copr_getter |
| 491 | + |
| 492 | + |
| 493 | +def restx_pagination(endpoint_method): |
| 494 | + """ |
| 495 | + Validates pagination arguments and converts pagination parameters from query to |
| 496 | + kwargs. |
| 497 | + """ |
| 498 | + @wraps(endpoint_method) |
| 499 | + def create_pagination(self, *args, **kwargs): |
| 500 | + kwargs = _shared_pagination_wrapper(**kwargs) |
| 501 | + return endpoint_method(self, *args, **kwargs) |
| 502 | + return create_pagination |
0 commit comments