Repository :
http://git.fedorahosted.org/cgit/copr.git
On branch : master
---------------------------------------------------------------
commit 2f07d9f691fa82df68a78a56ff4c0634c6b3324c
Author: Valentin Gologuzov <vgologuz(a)redhat.com>
Date: Tue Dec 16 19:26:49 2014 +0100
[frontend] REST API [WIP]
---------------------------------------------------------------
frontend/copr-frontend.spec | 4 +
frontend/coprs_frontend/coprs/__init__.py | 5 +
.../coprs_frontend/coprs/logic/complex_logic.py | 27 +++
frontend/coprs_frontend/coprs/logic/coprs_logic.py | 15 +-
frontend/coprs_frontend/coprs/models.py | 43 +++++
frontend/coprs_frontend/coprs/rest_api/__init__.py | 101 +++++++++++
.../coprs_frontend/coprs/rest_api/exceptions.py | 45 +++++
.../coprs/rest_api/resources}/__init__.py | 0
.../coprs/rest_api/resources/build.py | 89 ++++++++++
.../coprs/rest_api/resources/chroot.py | 54 ++++++
.../coprs/rest_api/resources/copr.py | 180 ++++++++++++++++++++
frontend/coprs_frontend/coprs/rest_api/schemas.py | 87 ++++++++++
frontend/coprs_frontend/coprs/rest_api/util.py | 54 ++++++
.../coprs/views/api_ns/api_general.py | 6 +-
.../coprs/views/coprs_ns/coprs_general.py | 7 +-
frontend/coprs_frontend/coprs/views/misc.py | 6 +
.../test_views/test_coprs_ns/test_coprs_general.py | 2 +-
frontend/requirements.txt | 3 +
18 files changed, 714 insertions(+), 14 deletions(-)
diff --git a/frontend/copr-frontend.spec b/frontend/copr-frontend.spec
index e01748b..def204d 100644
--- a/frontend/copr-frontend.spec
+++ b/frontend/copr-frontend.spec
@@ -53,6 +53,8 @@ Requires: python-whoosh >= 2.5.3
Requires: pytz
Requires: python-six
Requires: python-netaddr
+Requires: python-flask-restful
+Requires: python-marshmallow
# for tests:
Requires: pytest
Requires: python-flexmock
@@ -85,6 +87,8 @@ BuildRequires: python-mock
BuildRequires: python-decorator
BuildRequires: python-markdown
BuildRequires: pytz
+BuildRequires: python-flask-restful
+BuildRequires: python-marshmallow
%description
COPR is lightweight build system. It allows you to create new project in WebUI,
diff --git a/frontend/coprs_frontend/coprs/__init__.py
b/frontend/coprs_frontend/coprs/__init__.py
index fa02421..dd0eb05 100644
--- a/frontend/coprs_frontend/coprs/__init__.py
+++ b/frontend/coprs_frontend/coprs/__init__.py
@@ -70,3 +70,8 @@ app.register_blueprint(stats_receiver.stats_rcv_ns)
app.register_blueprint(tmp_ns.tmp_ns)
app.add_url_rule("/", "coprs_ns.coprs_show",
coprs_general.coprs_show)
+
+from rest_api import rest_api_bp, register_api_error_handler, URL_PREFIX
+register_api_error_handler(app)
+app.register_blueprint(rest_api_bp, url_prefix=URL_PREFIX)
+# register_api(app, db)
diff --git a/frontend/coprs_frontend/coprs/logic/complex_logic.py
b/frontend/coprs_frontend/coprs/logic/complex_logic.py
new file mode 100644
index 0000000..3f2bd53
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/logic/complex_logic.py
@@ -0,0 +1,27 @@
+# coding: utf-8
+
+import flask
+from .builds_logic import BuildsLogic
+from .coprs_logic import CoprsLogic
+
+
+class ComplexLogic(object):
+ """
+ Used for manipulation which affects multiply models
+ """
+
+ @classmethod
+ def delete_copr(cls, copr):
+ """
+ Delete copr and all its builds.
+
+ :param copr:
+ :raises ActionInProgressException:
+ :raises InsufficientRightsException:
+ """
+ builds_query = BuildsLogic.get_multiple_by_copr(copr=copr)
+
+ for build in builds_query:
+ BuildsLogic.delete_build(flask.g.user, build)
+
+ CoprsLogic.delete_unsafe(flask.g.user, copr)
diff --git a/frontend/coprs_frontend/coprs/logic/coprs_logic.py
b/frontend/coprs_frontend/coprs/logic/coprs_logic.py
index 9ea19dc..4e53e37 100644
--- a/frontend/coprs_frontend/coprs/logic/coprs_logic.py
+++ b/frontend/coprs_frontend/coprs/logic/coprs_logic.py
@@ -130,13 +130,13 @@ class CoprsLogic(object):
return query
@classmethod
- def add(cls, user, name, repos, selected_chroots, description,
- instructions, check_for_duplicates=False, **kwargs):
+ def add(cls, user, name, repos, selected_chroots, description=None,
+ instructions=None, check_for_duplicates=False, **kwargs):
copr = models.Copr(name=name,
repos=repos,
owner_id=user.id,
- description=description,
- instructions=instructions,
+ description=description or u"",
+ instructions=instructions or u"",
created_on=int(time.time()),
**kwargs)
@@ -188,7 +188,10 @@ class CoprsLogic(object):
db.session.add(copr)
@classmethod
- def delete(cls, user, copr, check_for_duplicates=True):
+ def delete_unsafe(cls, user, copr):
+ """
+ Deletes copr without termination of ongoing builds.
+ """
cls.raise_if_cant_delete(user, copr)
# TODO: do we want to dump the information somewhere, so that we can
# search it in future?
@@ -339,7 +342,9 @@ listen(models.Copr.auto_createrepo, 'set',
on_auto_createrepo_change,
class CoprChrootsLogic(object):
@classmethod
+
def mock_chroots_from_names(cls, names):
+
db_chroots = models.MockChroot.query.all()
mock_chroots = []
for ch in db_chroots:
diff --git a/frontend/coprs_frontend/coprs/models.py
b/frontend/coprs_frontend/coprs/models.py
index ca4d1af..15e77b7 100644
--- a/frontend/coprs_frontend/coprs/models.py
+++ b/frontend/coprs_frontend/coprs/models.py
@@ -1,3 +1,4 @@
+import copy
import datetime
import json
import os
@@ -170,6 +171,19 @@ class Copr(db.Model, helpers.Serializer):
}
@property
+ def yum_repos(self):
+ return {}
+ # todo: add config option to set backen results base url
+ # release_tmpl =
"{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
+ # result = {}
+ # for chroot in repo.active_chroots:
+ # release = release_tmpl.format(chroot=chroot)
+ # url = fix_protocol_for_backend(urlparse.urljoin(
+ # app.config["BACKEND_RESULTS_BASE_URL"], release +
'/'))
+ # result[release] = url
+ # return result
+
+ @property
def repos_list(self):
"""
Return repos of this copr as a list of strings
@@ -244,6 +258,13 @@ class Copr(db.Model, helpers.Serializer):
def full_name(self):
return "{}/{}".format(self.owner.username, self.name)
+ def to_dict(self, private=False, show_builds=True, show_chroots=True):
+ result = {}
+ for key in ["id", "name", "description",
"instructions"]:
+ result[key] = str(copy.copy(getattr(self, key)))
+ result["owner"] = self.owner.name
+ return result
+
class CoprPermission(db.Model, helpers.Serializer):
@@ -355,6 +376,13 @@ class Build(db.Model, helpers.Serializer):
return self.build_chroots[0].git_hash is None
@property
+ def repos_list(self):
+ if self.repos is None:
+ return list()
+ else:
+ return self.repos.split()
+
+ @property
def result_dir_name(self):
return "{:08d}-{}".format(self.id, self.package.name)
@@ -512,6 +540,15 @@ class Build(db.Model, helpers.Serializer):
else:
return src_rpm_name
+ def to_dict(self, options=None):
+ result = super(Build, self).to_dict(options)
+ result["src_pkg"] = result["pkgs"]
+ del result["pkgs"]
+ del result["copr_id"]
+
+ result["state"] = self.state
+ return result
+
class MockChroot(db.Model, helpers.Serializer):
@@ -556,6 +593,12 @@ class MockChroot(db.Model, helpers.Serializer):
"""
return "{0} {1}".format(self.os_release, self.os_version)
+ @property
+ def serializable_attributes(self):
+ attr_list = super(MockChroot, self).serializable_attributes
+ attr_list.extend(["name", "os"])
+ return attr_list
+
class CoprChroot(db.Model, helpers.Serializer):
diff --git a/frontend/coprs_frontend/coprs/rest_api/__init__.py
b/frontend/coprs_frontend/coprs/rest_api/__init__.py
new file mode 100644
index 0000000..309ba44
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/rest_api/__init__.py
@@ -0,0 +1,101 @@
+# coding: utf-8
+from flask import Response, url_for, Blueprint
+
+from flask_restful import Resource, Api
+from flask_restful_swagger import swagger
+
+from coprs.rest_api.exceptions import ApiError
+from coprs.rest_api.resources.build import BuildListR, BuildR
+from coprs.rest_api.resources.chroot import ChrootListR, ChrootR
+from coprs.rest_api.resources.copr import CoprListR, CoprR
+from coprs.rest_api.util import bp_url_for
+
+
+URL_PREFIX = "/api_2.0"
+
+
+class RootR(Resource):
+ @swagger.operation(
+ notes='List main API endpoints',
+ nickname='get',
+ )
+ def get(self):
+ return {
+ "links": {
+ "self": bp_url_for(RootR.endpoint),
+ "coprs": bp_url_for(CoprListR.endpoint),
+ "chroots": bp_url_for(ChrootListR.endpoint),
+ "builds": bp_url_for(BuildListR.endpoint),
+ }
+ }
+
+
+class MyApi(Api):
+ # flask-restfull error handling quite buggy right now
+ def error_router(self, original_handler, e):
+ return original_handler(e)
+ # def handle_error(self, e):
+ #
+ # if isinstance(e, sqlalchemy.orm.exc.NoResultFound):
+ # return self.make_response(str(e), 404)
+ #
+ #
+ # super(MyApi, self).handle_error(e)
+
+
+# def register_api(app, db):
+
+
+rest_api_bp = Blueprint("rest_api_bp",
+ __name__,
+ # app.import_name
+)
+
+api = MyApi(
+# api = Api(
+ # app,
+ rest_api_bp,
+ # prefix=URL_PREFIX,
+ catch_all_404s=True,
+
+)
+
+###################################
+# todo: maybe add later
+# Wrap the Api with swagger.docs. It is a thin wrapper around the Api class that adds
some swagger smarts
+# api = swagger.docs(
+# api,
+# # apiVersion='0.1',
+# # basePath=URL_PREFIX,
+# # resourcePath=URL_PREFIX,
+# # api_spec_url='{}/spec'.format(URL_PREFIX)
+# # api_spec_url='/spec',
+# # api_spec_url='{}/spec'.format(URL_PREFIX)
+# api_spec_url='/spec'
+# )
+###################################
+
+api.add_resource(RootR, "/")
+api.add_resource(CoprListR, "/coprs")
+api.add_resource(CoprR, "/coprs/<owner>/<project>")
+
+api.add_resource(ChrootListR, "/chroots")
+api.add_resource(ChrootR, "/chroots/<name>")
+
+api.add_resource(BuildListR, "/builds")
+api.add_resource(BuildR, "/builds/<int:build_id>")
+
+# app.register_blueprint(rest_api_bp, url_prefix=URL_PREFIX)
+
+
+# TODO: try:
https://github.com/sloria/flask-marshmallow
+def register_api_error_handler(app):
+ @app.errorhandler(ApiError)
+ def handle_api_error(error):
+ response = Response(
+ response="{}\n".format(error.data),
+ status=error.code,
+ mimetype="text/plain",
+ headers=error.headers,
+ )
+ return response
diff --git a/frontend/coprs_frontend/coprs/rest_api/exceptions.py
b/frontend/coprs_frontend/coprs/rest_api/exceptions.py
new file mode 100644
index 0000000..0fb14da
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/rest_api/exceptions.py
@@ -0,0 +1,45 @@
+# coding: utf-8
+import six
+
+
+class ApiError(Exception):
+ def __init__(self, code, data, **kwargs):
+ super(ApiError, self).__init__(**kwargs)
+
+ self.code = code
+ self.data = data
+
+ self.headers = kwargs.get("headers", {})
+
+ def __str__(self):
+ return str(self.data)
+
+ if six.PY2:
+ def __unicode__(self):
+ return unicode(self.data)
+
+
+class AuthFailed(ApiError):
+ def __init__(self, data, **kwargs):
+
+ super(AuthFailed, self).__init__(
+ 401, data,
+ # headers={"Authorization": "Basic"},
+ **kwargs)
+
+ self.headers["Authorization"] = "Basic"
+
+
+class ObjectNotFoundError(ApiError):
+ def __init__(self, data, **kwargs):
+ super(ObjectNotFoundError, self).__init__(404, data, **kwargs)
+
+
+class ObjectAlreadyExists(ApiError):
+ def __init__(self, data, **kwargs):
+ super(ObjectAlreadyExists, self).__init__(409, data, **kwargs)
+
+
+class MalformedRequest(ApiError):
+ def __init__(self, data=None, **kwargs):
+ super(MalformedRequest, self).__init__(400, data, **kwargs)
diff --git a/builder_image/src/__init__.py
b/frontend/coprs_frontend/coprs/rest_api/resources/__init__.py
similarity index 100%
copy from builder_image/src/__init__.py
copy to frontend/coprs_frontend/coprs/rest_api/resources/__init__.py
diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/build.py
b/frontend/coprs_frontend/coprs/rest_api/resources/build.py
new file mode 100644
index 0000000..1db3bf4
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/rest_api/resources/build.py
@@ -0,0 +1,89 @@
+# coding: utf-8
+
+import flask
+
+# from flask_restful_swagger import swagger
+
+from coprs.logic.coprs_logic import CoprsLogic
+from coprs.logic.builds_logic import BuildsLogic
+
+from coprs.rest_api.schemas import BuildSchema
+
+from coprs.rest_api.util import get_one_safe, bp_url_for
+
+from flask_restful import Resource, reqparse
+
+
+class BuildListR(Resource):
+
+ def get(self):
+
+ parser = reqparse.RequestParser()
+
+ parser.add_argument('owner', type=str,)
+ parser.add_argument('project', type=str)
+
+ parser.add_argument('limit', type=int)
+ parser.add_argument('offset', type=int)
+
+ req_args = parser.parse_args()
+
+ if "owner" and "project" in req_args:
+ query = BuildsLogic.get_multiple_by_name(
+ req_args["owner"], req_args["project"])
+ else:
+ query = BuildsLogic.get_multiple()
+
+ if "limit" in req_args:
+ limit = req_args["limit"]
+ else:
+ limit = 100
+
+ query = query.limit(limit)
+
+ if "offset" in req_args:
+ query = query.offset(req_args["offset"])
+
+ builds = query.all()
+ return {
+
+ "builds": [
+ {
+ "build": BuildSchema().dump(build)[0],
+ "link": bp_url_for(BuildR.endpoint, build_id=build.id),
+ } for build in builds
+ ],
+ "links": {
+ "self": bp_url_for(BuildListR.endpoint, **req_args),
+ },
+ }
+
+
+# @swagger.model
+# class BuildItem(object):
+# def __init__(self, build_id):
+# pass
+
+
+class BuildR(Resource):
+
+ def get(self, build_id):
+ """
+ Get single build by id
+ """
+ build = get_one_safe(BuildsLogic.get(build_id),
+ "Not found build with id: {}".format(build_id))
+ return {
+ "build": BuildSchema().dump(build)[0],
+ "links": {
+ "self": bp_url_for(BuildR.endpoint, build_id=build_id),
+ # TODO: can't do this due to circular imports
+ # "parent_copr": url_for(CoprR.endpoint,
+ # owner=build.copr.owner.name,
+ # project=build.copr.name),
+ }
+ }
+
+
+
+
diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/chroot.py
b/frontend/coprs_frontend/coprs/rest_api/resources/chroot.py
new file mode 100644
index 0000000..61d6e5a
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/rest_api/resources/chroot.py
@@ -0,0 +1,54 @@
+# coding: utf-8
+
+import json
+import flask
+from flask import url_for
+from flask_restful import Resource, reqparse
+
+from marshmallow import Schema, fields, pprint
+
+from coprs.views.misc import api_login_required
+from coprs.logic.coprs_logic import MockChrootsLogic, CoprChrootsLogic, CoprsLogic
+
+from ..util import get_one_safe, json_loads_safe, mm_deserialize, bp_url_for
+
+
+class ChrootListR(Resource):
+ def get(self):
+ chroots = MockChrootsLogic.get_multiple(None).all()
+ return {
+ "links": {
+ "self": bp_url_for(ChrootListR.endpoint),
+ },
+ "chroots": [
+ {
+ "chroot": chroot.to_dict(),
+ "link": url_for(ChrootR.endpoint, name=chroot.name),
+ } for chroot in chroots
+ ]
+ }
+
+
+class ChrootR(Resource):
+ def get(self, name):
+ chroot = get_one_safe(MockChrootsLogic.get_from_name(name))
+ return {
+ "chroot": chroot.to_dict(),
+ "links": {
+ "self": bp_url_for(ChrootR.endpoint, name=chroot.name)
+ },
+ }
+
+
+class BuildChrootR(Resource):
+ def get(self, owner, project, name):
+ copr = get_one_safe(CoprsLogic.get(flask.g.user, owner, project),
+ "Copr {}/{} not found".format(owner, project))
+ chroot = get_one_safe(CoprChrootsLogic.get(copr, name))
+
+ return {
+ "chroot": chroot.to_dict(),
+ "links": {
+ "self": bp_url_for(BuildChrootR.endpoint, owner=owner,
project=project, name=name)
+ }
+ }
diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/copr.py
b/frontend/coprs_frontend/coprs/rest_api/resources/copr.py
new file mode 100644
index 0000000..f1f5e48
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/rest_api/resources/copr.py
@@ -0,0 +1,180 @@
+import base64
+import datetime
+import functools
+
+import flask
+from flask_restful import Resource, reqparse
+from marshmallow import pprint
+
+from coprs import db
+from coprs.exceptions import DuplicateException
+from coprs.logic.complex_logic import ComplexLogic
+from coprs.logic.users_logic import UsersLogic
+from coprs.logic.coprs_logic import CoprsLogic
+from coprs.exceptions import ActionInProgressException, InsufficientRightsException
+from .build import BuildListR
+from coprs.rest_api.schemas import CoprSchema
+from ..exceptions import ObjectAlreadyExists, AuthFailed
+from ..util import get_one_safe, json_loads_safe, mm_deserialize, bp_url_for,
render_allowed_method
+
+
+def rest_api_auth_required(f):
+ @functools.wraps(f)
+ def decorated_function(*args, **kwargs):
+ token = None
+ apt_login = None
+ if "Authorization" in flask.request.headers:
+ base64string = flask.request.headers["Authorization"]
+ base64string = base64string.split()[1].strip()
+ userstring = base64.b64decode(base64string)
+ (apt_login, token) = userstring.split(":")
+ token_auth = False
+ if token and apt_login:
+ user = UsersLogic.get_by_api_login(apt_login).first()
+ if (user and user.api_token == token and
+ user.api_token_expiration >= datetime.date.today()):
+
+ token_auth = True
+ flask.g.user = user
+ if not token_auth:
+ message = (
+ "Login invalid/expired. "
+ "Please visit
https://copr.fedoraproject.org/api "
+ "get or renew your API token.")
+
+ raise AuthFailed(message)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+class CoprListR(Resource):
+
+ @rest_api_auth_required
+ def post(self):
+ """
+ Creates new copr
+ """
+ owner = flask.g.user
+ # new_copr_dict = json_loads_safe(flask.request.data, "")
+
+ result = mm_deserialize(CoprSchema(), flask.request.data)
+ # todo check that chroots are available
+ pprint(result.data)
+ try:
+ CoprsLogic.add(user=owner, check_for_duplicates=True, **result.data)
+ db.session.commit()
+ except DuplicateException as error:
+ raise ObjectAlreadyExists(data=error)
+
+ return "New copr was created", 201
+
+ def get(self):
+ """
+ Get coprs collection
+ :return:
+ """
+ parser = reqparse.RequestParser()
+ parser.add_argument('owner', dest='username', type=str)
+ parser.add_argument('limit', type=int)
+ parser.add_argument('offset', type=int)
+ req_args = parser.parse_args()
+
+ kwargs = {}
+ for key in ["username"]:
+ if req_args[key]:
+ kwargs[key] = req_args[key]
+
+ if "username" in kwargs:
+ query =
CoprsLogic.get_multiple_owned_by_username(req_args["username"])
+ else:
+ query = CoprsLogic.get_multiple(flask.g.user)
+
+ # todo: also could be optional
+ query = CoprsLogic.join_builds(query)
+
+ if req_args["offset"]:
+ query = query.offset(req_args["offset"])
+
+ # todo: add maximum allowed limit and also use as a default limit
+ if req_args["limit"]:
+ query = query.limit(req_args["limit"])
+
+ coprs_list = query.all()
+
+ return {
+ "links": {
+ "self": bp_url_for(CoprListR.endpoint, **req_args)
+ },
+ "coprs": [
+ {
+ "copr": CoprSchema().dump(copr)[0],
+ "link": bp_url_for(
+ CoprR.endpoint,
+ owner=copr.owner.name,
+ project=copr.name
+ ),
+ }
+ for copr in coprs_list
+ ],
+ # TODO: show only if user provided ?help=true param
+ # "allowed_methods": [
+ # render_allowed_method("GET", "Get list of coprs",
require_auth=False,
+ # params=[
+ # "username: filter coprs owned by
user",
+ # "limit: show only the given number of
coprs",
+ # "offset: skip given number of
coprs",
+ # ]),
+ # render_allowed_method("POST", "Creates new copr, send
dict with copr fields"),
+ # ]
+ }
+
+
+class CoprR(Resource):
+
+ @rest_api_auth_required
+ def delete(self, owner, project):
+ copr = get_one_safe(CoprsLogic.get(flask.g.user, owner, project),
+ "Copr {}/{} not found".format(owner, project))
+ try:
+ ComplexLogic.delete_copr(copr)
+ except (ActionInProgressException,
+ InsufficientRightsException) as err:
+ db.session.rollback()
+ raise
+ else:
+ db.session.commit()
+
+ return None, 204
+
+ def get(self, owner, project):
+ # parser = reqparse.RequestParser()
+ # parser.add_argument('show_builds', type=bool, default=True)
+ # parser.add_argument('show_chroots', type=bool, default=True)
+ # req_args = parser.parse_args()
+
+ copr = get_one_safe(CoprsLogic.get(flask.g.user, owner, project),
+ "Copr {}/{} not found".format(owner, project))
+ return {
+ "copr": CoprSchema().dump(copr)[0],
+ "links": {
+ "self": bp_url_for(CoprR.endpoint,
+ owner=owner,
+ project=project),
+ # "chroots":
+ "builds": bp_url_for(BuildListR.endpoint,
+ owner=copr.owner.name,
+ project=copr.name)
+ },
+ # "allowed_methods": [
+ # render_allowed_method("GET", "Get single copr",
require_auth=False),
+ # render_allowed_method("DELETE", "Delete current
copr", require_auth=True),
+ # ]
+ }
+
+ @rest_api_auth_required
+ def put(self, owner, project):
+ """
+ Modifies project by replacment of provided fields
+ """
+ pass
+
diff --git a/frontend/coprs_frontend/coprs/rest_api/schemas.py
b/frontend/coprs_frontend/coprs/rest_api/schemas.py
new file mode 100644
index 0000000..9c243a7
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/rest_api/schemas.py
@@ -0,0 +1,87 @@
+# coding: utf-8
+from marshmallow import Schema, fields
+
+
+class AllowedMethodSchema(Schema):
+ method = fields.Str()
+ doc = fields.Str()
+ require_auth = fields.Bool()
+ params = fields.List(fields.Str())
+
+
+class MockChroot(Schema):
+ pass
+
+
+class CoprSchema(Schema):
+ name = fields.Str(required=True)
+ description = fields.Str()
+ instructions = fields.Str()
+
+ auto_createrepo = fields.Bool()
+ build_enable_net = fields.Bool()
+ last_modified = fields.DateTime()
+
+ additional_repos = fields.List(fields.Str, dump_only=True,
attribute="repos_list")
+ # yum_repos = fields.List()
+
+ # used only for creation
+ chroots_to_enable = fields.List(fields.Str, load_only=True)
+
+ _keys_to_make_object = [
+ "description",
+ "instructions",
+ "auto_createrepo"
+ ]
+
+ def make_object(self, data):
+ """
+ Create kwargs for CoprsLogic.add
+ """
+ kwargs = dict(
+ name=data["name"].strip(),
+ repos=" ".join(data.get("repos", [])),
+ selected_chroots=data["chroots"],
+ )
+ for key in self._keys_to_make_object:
+ if key in data:
+ kwargs[key] = data[key]
+ return kwargs
+
+
+class BuildChrootSchema(Schema):
+ # used only for presentation
+ state = fields.Str()
+ started_on = fields.Int()
+ ended_on = fields.Int()
+ git_hash = fields.Str()
+
+
+class BuildSchema(Schema):
+
+ id = fields.Int()
+ pkgs = fields.Str()
+ build_packages = fields.Str()
+ pkg_version = fields.Str()
+
+ repos_list = fields.List(fields.Str())
+ repos = fields.Str() # legacy
+
+ submitted_on = fields.Int()
+ started_on = fields.Int()
+ ended_on = fields.Int()
+
+ results = fields.Str()
+ memory_reqs = fields.Int()
+ timeout = fields.Int()
+
+ enable_net = fields.Bool()
+
+ source_type = fields.Int()
+ source_json = fields.Str()
+
+ # chroots = fields.List(fields.Nested(BuildChrootSchema))
+
+
+
+
diff --git a/frontend/coprs_frontend/coprs/rest_api/util.py
b/frontend/coprs_frontend/coprs/rest_api/util.py
new file mode 100644
index 0000000..ac9b0b6
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/rest_api/util.py
@@ -0,0 +1,54 @@
+# coding: utf-8
+import json
+
+import sqlalchemy.orm.exc
+from .exceptions import ObjectNotFoundError, MalformedRequest
+from schemas import AllowedMethodSchema
+from flask import Response, url_for, Blueprint
+
+
+class AllowedMethod(object):
+ def __init__(self, method, doc, require_auth=True, params=None):
+ self.method = method
+ self.doc = doc
+ self.require_auth = require_auth
+ self.params = params or []
+
+
+def render_allowed_method(method, doc, require_auth=True, params=None):
+ return AllowedMethodSchema().dump(AllowedMethod(method, doc, require_auth,
params))[0]
+
+
+def bp_url_for(endpoint, *args, **kwargs):
+ """
+ Prepend endpoing with dot
+ :param endpoint:
+ :param args:
+ :param kwargs:
+ :return:
+ """
+
+ return url_for(".{}".format(endpoint), *args, **kwargs)
+
+
+def get_one_safe(query, data_on_error=None):
+ try:
+ return query.one()
+ except sqlalchemy.orm.exc.NoResultFound:
+ raise ObjectNotFoundError(data_on_error)
+
+
+def json_loads_safe(raw, data_on_error=None):
+ try:
+ return json.loads(raw)
+ except ValueError:
+ raise MalformedRequest(data_on_error or
+ "Failed to deserialize json string")
+
+
+def mm_deserialize(schema, obj_dict):
+ result = schema.loads(obj_dict)
+ if result.errors:
+ raise MalformedRequest(data=result.errors)
+ # import ipdb; ipdb.set_trace()
+ return result
diff --git a/frontend/coprs_frontend/coprs/views/api_ns/api_general.py
b/frontend/coprs_frontend/coprs/views/api_ns/api_general.py
index 2070ae2..b9f220b 100644
--- a/frontend/coprs_frontend/coprs/views/api_ns/api_general.py
+++ b/frontend/coprs_frontend/coprs/views/api_ns/api_general.py
@@ -16,6 +16,7 @@ from coprs import forms
from coprs import helpers
from coprs.helpers import fix_protocol_for_backend
from coprs.logic.api_logic import MonitorWrapper
+from coprs.logic.complex_logic import ComplexLogic
from coprs.views.misc import login_required, api_login_required
@@ -148,11 +149,8 @@ def api_copr_delete(username, coprname):
httpcode = 200
if form.validate_on_submit() and copr:
- builds_query = builds_logic.BuildsLogic.get_multiple_by_copr(copr=copr)
try:
- for build in builds_query:
- builds_logic.BuildsLogic.delete_build(flask.g.user, build)
- CoprsLogic.delete(flask.g.user, copr)
+ ComplexLogic.delete_copr(copr)
except (exceptions.ActionInProgressException,
exceptions.InsufficientRightsException) as err:
output = {"output": "notok", "error": err}
diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
index 6a69513..8a68e43 100644
--- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
+++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
@@ -22,6 +22,8 @@ from coprs import models
from coprs.logic.stat_logic import CounterStatLogic
from coprs.rmodels import TimedStatEvents
+from coprs.logic.complex_logic import ComplexLogic
+
from coprs.views.misc import login_required, page_not_found
from coprs.views.coprs_ns import coprs_ns
@@ -507,12 +509,9 @@ def copr_delete(username, coprname):
copr = coprs_logic.CoprsLogic.get(username, coprname).first()
if form.validate_on_submit() and copr:
- builds_query = builds_logic.BuildsLogic.get_multiple_by_copr(copr=copr)
try:
- for build in builds_query:
- builds_logic.BuildsLogic.delete_build(flask.g.user, build)
- coprs_logic.CoprsLogic.delete(flask.g.user, copr)
+ ComplexLogic.delete_copr(copr)
except (exceptions.ActionInProgressException,
exceptions.InsufficientRightsException) as e:
diff --git a/frontend/coprs_frontend/coprs/views/misc.py
b/frontend/coprs_frontend/coprs/views/misc.py
index 38d0ab5..b0a98d7 100644
--- a/frontend/coprs_frontend/coprs/views/misc.py
+++ b/frontend/coprs_frontend/coprs/views/misc.py
@@ -51,6 +51,11 @@ def krb_strip_realm(fullname):
@app.before_request
+def set_empty_user():
+ flask.g.user = None
+
+
+(a)app.before_request
def lookup_current_user():
flask.g.user = username = None
if "openid" in flask.session:
@@ -222,6 +227,7 @@ def api_login_required(f):
return decorated_function
+
def login_required(role=helpers.RoleEnum("user")):
def view_wrapper(f):
@functools.wraps(f)
diff --git a/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py
b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py
index da3f778..bd06cd9 100644
--- a/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py
+++ b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py
@@ -39,7 +39,7 @@ class TestMonitor(CoprsTestCase):
res = self.tc.get("/coprs/{}/{}/monitor/".format(self.u1.name,
copr_name))
assert res.status_code == 200
- self.db.session.add(CoprsLogic.delete(self.u1, tmp_copr))
+ self.db.session.add(CoprsLogic.delete_unsafe(self.u1, tmp_copr))
self.db.session.commit()
res = self.tc.get("/coprs/{}/{}/monitor/".format(self.u1.name,
copr_name))
diff --git a/frontend/requirements.txt b/frontend/requirements.txt
index 82354b3..8c3653f 100644
--- a/frontend/requirements.txt
+++ b/frontend/requirements.txt
@@ -17,3 +17,6 @@ decorator
python-dateutil
netaddr
alembic
+flask-restful
+# flask-restful-swagger # -- nice to have, but probably later
+marshmallow