Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
nikromen committed Nov 19, 2023
1 parent 0002ed7 commit fe32d34
Show file tree
Hide file tree
Showing 10 changed files with 1,128 additions and 875 deletions.
208 changes: 164 additions & 44 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@
from functools import wraps
from werkzeug.datastructures import ImmutableMultiDict, MultiDict
from sqlalchemy.orm.attributes import InstrumentedAttribute
from flask_restx import Api
from flask_restx import Api, Namespace
from coprs.exceptions import (
AccessRestricted,
ActionInProgressException,
CoprHttpException,
InsufficientStorage,
ObjectNotFound,
BadRequest,
)
from coprs.logic.complex_logic import ComplexLogic
Expand Down Expand Up @@ -50,48 +47,68 @@ def home():
# HTTP methods
GET = ["GET"]
POST = ["POST"]
# TODO: POST != PUT nor DELETE, we should use at least use these methods according
# conventions -> POST to create new element, PUT to update element, DELETE to delete
# https://www.ibm.com/docs/en/urbancode-release/6.1.1?topic=reference-rest-api-conventions
PUT = ["POST", "PUT"]
DELETE = ["POST", "DELETE"]


def _convert_path_params_to_query(endpoint_method, params_to_not_look_for, **kwargs):
sig = inspect.signature(endpoint_method)
params = [x for x in sig.parameters]
params = list(set(params) - params_to_not_look_for)
for arg in params:
if arg not in flask.request.args:
# If parameter is present in the URL path, we can use its
# value instead of failing that it is missing in query
# parameters, e.g. let's have a view decorated with these
# two routes:
# @foo_ns.route("/foo/bar/<int:build>/<chroot>")
# @foo_ns.route("/foo/bar") accepting ?build=X&chroot=Y
# @query_params()
# Then we need the following condition to get the first
# route working
if arg in flask.request.view_args:
continue

# If parameter has a default value, it is not required
default_parameter_value = sig.parameters[arg].default
if default_parameter_value != sig.parameters[arg].empty:
kwargs[arg] = default_parameter_value
continue

raise BadRequest("Missing argument {}".format(arg))

kwargs[arg] = flask.request.args.get(arg)
return kwargs


def query_params():
params_to_not_look_for = {"args", "kwargs"}

def query_params_decorator(f):
@wraps(f)
def query_params_wrapper(*args, **kwargs):
sig = inspect.signature(f)
params = [x for x in sig.parameters]
params = list(set(params) - {"args", "kwargs"})
for arg in params:
if arg not in flask.request.args:
# If parameter is present in the URL path, we can use its
# value instead of failing that it is missing in query
# parameters, e.g. let's have a view decorated with these
# two routes:
# @foo_ns.route("/foo/bar/<int:build>/<chroot>")
# @foo_ns.route("/foo/bar") accepting ?build=X&chroot=Y
# @query_params()
# Then we need the following condition to get the first
# route working
if arg in flask.request.view_args:
continue

# If parameter has a default value, it is not required
if sig.parameters[arg].default == sig.parameters[arg].empty:
raise BadRequest("Missing argument {}".format(arg))
kwargs[arg] = flask.request.args.get(arg)
kwargs = _convert_path_params_to_query(f, params_to_not_look_for, **kwargs)
return f(*args, **kwargs)
return query_params_wrapper
return query_params_decorator


def _shared_pagination_wrapper(**kwargs):
form = PaginationForm(flask.request.args)
if not form.validate():
raise CoprHttpException(form.errors)
kwargs.update(form.data)
return kwargs


def pagination():
def pagination_decorator(f):
@wraps(f)
def pagination_wrapper(*args, **kwargs):
form = PaginationForm(flask.request.args)
if not form.validate():
raise CoprHttpException(form.errors)
kwargs.update(form.data)
kwargs = _shared_pagination_wrapper(**kwargs)
return f(*args, **kwargs)
return pagination_wrapper
return pagination_decorator
Expand Down Expand Up @@ -247,21 +264,6 @@ def wrapper(ownername, projectname):
return wrapper


# TODO: sync editable_copr decorator with flask-restx once everything's migrated
def get_editable_copr(func):
"""
Apply @editable_copr decorator on endpoints migrated to flask-restx.
"""
@wraps(func)
def editable_copr_getter(*args, ownername, projectname):
def get_copr_from_dec(return_copr_from_decorator):
return return_copr_from_decorator

copr = editable_copr(get_copr_from_dec)(ownername, projectname)
return func(*args, copr)
return editable_copr_getter


def set_defaults(formdata, form_class):
"""
Take a `formdata` which can be `flask.request.form` or an output from
Expand Down Expand Up @@ -388,3 +390,121 @@ def rename_fields_helper(input_dict, replace):
for value in values:
output.add(new_key, value)
return output


# Flask-restx specific decorator - don't use them with regular Flask API!
# TODO: delete/unify decorators for regular Flask and Flask-restx API once migration
# is done


def path_to_query(endpoint_method):
"""
Decorator converting path parameters to query parameters
Usage:
class Endpoint(Resource):
...
@path_to_query
...
def get():
return {"scary": "BOO!"}
Returns:
Endpoint that has its path parameters converted as query parameters.
"""
params_to_not_look_for = {"self", "args", "kwargs"}

@wraps(endpoint_method)
def convert_path_parameters_of_endpoint_method(self, *args, **kwargs):
kwargs = _convert_path_params_to_query(endpoint_method, params_to_not_look_for, **kwargs)
return endpoint_method(self, *args, **kwargs)
return convert_path_parameters_of_endpoint_method


def deprecated_route_method(ns: Namespace, msg):
"""
Decorator that display a deprecation warning in headers and docs.
Usage:
class Endpoint(Resource):
...
@deprecated_endpoint("POST", "PUT")
...
def get():
return {"scary": "BOO!"}
Args:
ns: flask-restx Namespace
msg: Deprecation warning message.
"""
def decorate_endpoint_method(endpoint_method):
# render deprecation in API docs
ns.deprecated(endpoint_method)

@wraps(endpoint_method)
def warn_user_in_headers(self, *args, **kwargs):
custom_header = {"Warning": f"This method is deprecated: {msg}"}
resp = endpoint_method(self, *args, **kwargs)
if not isinstance(resp, tuple):
# only resp body as dict was passed
return resp + (custom_header,)

for part_of_resp in resp[1:]:
if isinstance(part_of_resp, dict):
part_of_resp |= custom_header
return resp

return resp + (custom_header,)

return warn_user_in_headers
return decorate_endpoint_method


def deprecated_route_method_type(ns: Namespace, deprecated_method_type: str, use_instead: str):
"""
Calls deprecated_route decorator with specific message about deprecated method.
Usage:
class Endpoint(Resource):
...
@deprecated_method("POST", "PUT")
...
def get():
return {"scary": "BOO!"}
Args:
ns: flask-restx Namespace
deprecated_method_type: method enum e.g. POST
use_instead: method user should use instead
"""
def call_deprecated_endpoint_method(endpoint_method):
msg = f"Use {use_instead} method instead of {deprecated_method_type}"
return deprecated_route_method(ns, msg)(endpoint_method)
return call_deprecated_endpoint_method


def restx_editable_copr(endpoint_method):
"""
Raise an exception if user don;t have permissions for editing Copr repo.
"""
@wraps(endpoint_method)
def editable_copr_getter(self, ownername, projectname):
copr = editable_copr(endpoint_method)(ownername, projectname)
return endpoint_method(self, copr)
return editable_copr_getter


def restx_pagination(endpoint_method):
"""
Args:
endpoint_method:
Returns:
"""
@wraps(endpoint_method)
def create_pagination(self, *args, **kwargs):
kwargs = _shared_pagination_wrapper(**kwargs)
return endpoint_method(self, *args, **kwargs)
return create_pagination
8 changes: 3 additions & 5 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
from coprs.exceptions import (BadRequest, AccessRestricted)
from coprs.views.misc import api_login_required
from coprs.views.apiv3_ns import apiv3_ns, api, rename_fields_helper
from coprs.views.apiv3_ns.schema import (
build_model,
get_build_params,
)
from coprs.views.apiv3_ns.schema.schemas import build_model
from coprs.views.apiv3_ns.schema.docs import get_build_docs
from coprs.logic.complex_logic import ComplexLogic
from coprs.logic.builds_logic import BuildsLogic
from coprs.logic.coprs_logic import CoprDirsLogic
Expand Down Expand Up @@ -93,7 +91,7 @@ def render_build(build):
@apiv3_builds_ns.route("/<int:build_id>")
class GetBuild(Resource):

@apiv3_builds_ns.doc(params=get_build_params)
@apiv3_builds_ns.doc(params=get_build_docs)
@apiv3_builds_ns.marshal_with(build_model)
def get(self, build_id):
"""
Expand Down
33 changes: 13 additions & 20 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,16 @@
UnknownSourceTypeException,
InvalidForm,
)
from coprs.views.misc import api_login_required
from coprs.views.misc import api_login_required, restx_api_login_required
from coprs import db, models, forms, helpers
from coprs.views.apiv3_ns import apiv3_ns, api, rename_fields_helper
from coprs.views.apiv3_ns.schema import (
from coprs.views.apiv3_ns.schema.schemas import (
package_model,
add_package_params,
edit_package_params,
get_package_parser,
add_package_parser,
edit_package_parser,
package_get_input_model,
package_add_input_model,
package_edit_input_model,
)
from coprs.views.apiv3_ns.schema.docs import add_package_docs, edit_package_docs
from coprs.logic.packages_logic import PackagesLogic

# @TODO if we need to do this on several places, we should figure a better way to do it
Expand Down Expand Up @@ -110,9 +109,7 @@ def get_arg_to_bool(argument):

@apiv3_packages_ns.route("/")
class GetPackage(Resource):
parser = get_package_parser()

@apiv3_packages_ns.expect(parser)
@apiv3_packages_ns.expect(package_get_input_model)
@apiv3_packages_ns.marshal_with(package_model)
def get(self):
"""
Expand Down Expand Up @@ -171,11 +168,9 @@ def get_package_list(ownername, projectname, with_latest_build=False,

@apiv3_packages_ns.route("/add/<ownername>/<projectname>/<package_name>/<source_type_text>")
class PackageAdd(Resource):
parser = add_package_parser()

@api_login_required
@apiv3_packages_ns.doc(params=add_package_params)
@apiv3_packages_ns.expect(parser)
@restx_api_login_required
@apiv3_packages_ns.doc(params=add_package_docs)
@apiv3_packages_ns.expect(package_add_input_model)
@apiv3_packages_ns.marshal_with(package_model)
def post(self, ownername, projectname, package_name, source_type_text):
"""
Expand All @@ -195,11 +190,9 @@ def post(self, ownername, projectname, package_name, source_type_text):
@apiv3_packages_ns.route("/edit/<ownername>/<projectname>/<package_name>/")
@apiv3_packages_ns.route("/edit/<ownername>/<projectname>/<package_name>/<source_type_text>")
class PackageEdit(Resource):
parser = edit_package_parser()

@api_login_required
@apiv3_packages_ns.doc(params=edit_package_params)
@apiv3_packages_ns.expect(parser)
@restx_api_login_required
@apiv3_packages_ns.doc(params=edit_package_docs)
@apiv3_packages_ns.expect(package_edit_input_model)
@apiv3_packages_ns.marshal_with(package_model)
def post(self, ownername, projectname, package_name, source_type_text=None):
"""
Expand Down
Loading

0 comments on commit fe32d34

Please sign in to comment.