Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : master
commit a61ba325517ffe0a15b4b72a387b1e48a16a113d Author: Valentin Gologuzov vgologuz@redhat.com Date: Tue Sep 8 18:15:14 2015 +0200
[frontend][API] replaced BuildChroot with BuildTask, tests are broken and documentation should reflect this changes
.../coprs_frontend/coprs/logic/builds_logic.py | 30 ++++++ frontend/coprs_frontend/coprs/rest_api/__init__.py | 38 ++++--- frontend/coprs_frontend/coprs/rest_api/common.py | 10 +- .../coprs/rest_api/resources/build.py | 31 +++--- .../coprs/rest_api/resources/build_chroot.py | 47 -------- .../coprs/rest_api/resources/build_task.py | 111 ++++++++++++++++++++ .../coprs/rest_api/resources/mock_chroot.py | 4 +- .../coprs/rest_api/resources/project.py | 11 +- frontend/coprs_frontend/coprs/rest_api/schemas.py | 5 +- frontend/coprs_frontend/coprs/rest_api/util.py | 25 ++++- .../Resources/{build_chroot.rst => build_task.rst} | 43 +++++--- frontend/docs/api_2/source/index.rst | 2 +- 12 files changed, 247 insertions(+), 110 deletions(-)
diff --git a/frontend/coprs_frontend/coprs/logic/builds_logic.py b/frontend/coprs_frontend/coprs/logic/builds_logic.py index ace4bfa..3992ed0 100644 --- a/frontend/coprs_frontend/coprs/logic/builds_logic.py +++ b/frontend/coprs_frontend/coprs/logic/builds_logic.py @@ -9,6 +9,7 @@ import time import flask from sqlalchemy import or_ from sqlalchemy import and_ +from sqlalchemy.orm import joinedload from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.sql import false from werkzeug.utils import secure_filename @@ -37,6 +38,7 @@ class BuildsLogic(object): def get(cls, build_id): return models.Build.query.filter(models.Build.id == build_id)
+ # todo: move methods operating with BuildChroot to BuildChrootLogic @classmethod def get_build_tasks(cls, status): return models.BuildChroot.query.filter(models.BuildChroot.status == status) \ @@ -521,6 +523,34 @@ class BuildChrootsLogic(object): .filter(BuildChroot.mock_chroot_id == mc.id) )
+ @classmethod + def get_multiply(cls): + query = ( + models.BuildChroot.query + .join(models.BuildChroot.build) + .join(models.BuildChroot.mock_chroot) + .join(models.Build.copr) + .join(models.Copr.owner) + + ) + return query + + @classmethod + def filter_by_build_id(cls, query, build_id): + return query.filter(models.Build.id == build_id) + + @classmethod + def filter_by_project_id(cls, query, project_id): + return query.filter(models.Copr.id == project_id) + + @classmethod + def filter_by_project_owner_name(cls, query, username): + return query.filter(models.User.username == username) + + @classmethod + def filter_by_state(cls, query, state): + return query.filter(models.BuildChroot.status == StatusEnum(state)) +
class BuildsMonitorLogic(object): @classmethod diff --git a/frontend/coprs_frontend/coprs/rest_api/__init__.py b/frontend/coprs_frontend/coprs/rest_api/__init__.py index 7276c2f..0f87c4f 100644 --- a/frontend/coprs_frontend/coprs/rest_api/__init__.py +++ b/frontend/coprs_frontend/coprs/rest_api/__init__.py @@ -7,7 +7,7 @@ from coprs.exceptions import InsufficientRightsException
from coprs.rest_api.exceptions import ApiError from coprs.rest_api.resources.build import BuildListR, BuildR -from coprs.rest_api.resources.build_chroot import BuildChrootListR, BuildChrootR +from coprs.rest_api.resources.build_task import BuildTaskListR, BuildTaskR from coprs.rest_api.resources.mock_chroot import MockChrootListR, MockChrootR from coprs.rest_api.resources.project import ProjectListR, ProjectR from coprs.rest_api.resources.project_chroot import ProjectChrootListR, ProjectChrootR @@ -20,9 +20,19 @@ class RootR(Resource): return { "_links": { "self": {"href": url_for(".rootr")}, - "projects": {"href": url_for(".projectlistr")}, + "projects": { + "href": url_for(".projectlistr"), + "query_params": [ + { + "name": u"owner", + "type": u"string", + "description": u"Select only project owned by the user" + } + ] + }, "mock_chroots": {"href": url_for(".mockchrootlistr")}, "builds": {"href": url_for(".buildlistr")}, + "build_tasks": {"href": url_for(".buildtasklistr")}, } }
@@ -30,14 +40,15 @@ class RootR(Resource): class MyApi(Api): # flask-restfull error handling quite buggy right now def error_router(self, original_handler, e): + # import ipdb; ipdb.set_trace() 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 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): @@ -59,11 +70,10 @@ api.add_resource(BuildR, "/builds/int:build_id") api.add_resource(ProjectChrootListR, "/projects/int:project_id/chroots") api.add_resource(ProjectChrootR, "/projects/int:project_id/chroots/<name>")
-api.add_resource(BuildChrootListR, "/builds/int:build_id/chroots") -api.add_resource(BuildChrootR, "/builds/int:build_id/chroots/<name>")
- -# app.register_blueprint(rest_api_bp, url_prefix=URL_PREFIX) +api.add_resource(BuildTaskListR, "/build_tasks") +# todo: add redirect from /build_tasks/int:build_id -> /build_tasks?build_id=<build_id> +api.add_resource(BuildTaskR, "/build_tasks/int:build_id/<name>")
def register_api_error_handler(app): @@ -72,7 +82,7 @@ def register_api_error_handler(app): """ :param ApiError error: """ - + # import ipdb; ipdb.set_trace() content = { "message": error.msg, } diff --git a/frontend/coprs_frontend/coprs/rest_api/common.py b/frontend/coprs_frontend/coprs/rest_api/common.py index 72c5eae..7bc9c81 100644 --- a/frontend/coprs_frontend/coprs/rest_api/common.py +++ b/frontend/coprs_frontend/coprs/rest_api/common.py @@ -5,7 +5,7 @@ import functools from logging import getLogger from coprs.logic.builds_logic import BuildsLogic from coprs.logic.coprs_logic import CoprsLogic -from coprs.rest_api.schemas import BuildChrootSchema +from coprs.rest_api.schemas import BuildTaskSchema from coprs.rest_api.util import mm_serialize_one, get_one_safe
log = getLogger(__name__) @@ -41,7 +41,7 @@ def render_build(build, self_params=None): "_links": { "self": {"href": url_for(".buildr", build_id=build.id, **self_params)}, "project": {"href": url_for(".projectr", project_id=build.copr_id)}, - "chroots": {"href": url_for(".buildchrootlistr", build_id=build.id)} + "tasks": {"href": url_for(".buildtasklistr", build_id=build.id)} } }
@@ -60,15 +60,15 @@ def render_project(copr, self_params=None): }
-def render_build_chroot(chroot): +def render_build_task(chroot): """ :type chroot: models.BuildChroot """ return { - "chroot": mm_serialize_one(BuildChrootSchema, chroot), + "task": mm_serialize_one(BuildTaskSchema, chroot), "_links": { "project": {"href": url_for(".projectr", project_id=chroot.build.copr_id)}, - "self": {"href": url_for(".buildchrootr", + "self": {"href": url_for(".buildtaskr", build_id=chroot.build.id, name=chroot.name)}, } diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/build.py b/frontend/coprs_frontend/coprs/rest_api/resources/build.py index 6f4a792..7d31412 100644 --- a/frontend/coprs_frontend/coprs/rest_api/resources/build.py +++ b/frontend/coprs_frontend/coprs/rest_api/resources/build.py @@ -2,28 +2,24 @@
import flask from flask import url_for, make_response - -# from flask_restful_swagger import swagger - -from coprs import db -from coprs.exceptions import ActionInProgressException, InsufficientRightsException, RequestCannotBeExecuted -from coprs.logic.builds_logic import BuildsLogic -from coprs.rest_api.common import get_project_safe -from coprs.rest_api.exceptions import MalformedRequest, CannotProcessRequest, AccessForbidden -from ..common import render_build, rest_api_auth_required, render_build_chroot, get_build_safe, get_user_safe - -from coprs.rest_api.schemas import BuildSchema, BuildCreateSchema, BuildCreateFromUrlSchema - -from coprs.rest_api.util import mm_deserialize - from flask_restful import Resource, reqparse
+from ... import db +from ...exceptions import ActionInProgressException, InsufficientRightsException, RequestCannotBeExecuted +from ...helpers import StatusEnum +from ...logic.builds_logic import BuildsLogic +from ..common import get_project_safe +from ..exceptions import MalformedRequest, CannotProcessRequest, AccessForbidden +from ..common import render_build, rest_api_auth_required, render_build_task, get_build_safe, get_user_safe +from ..schemas import BuildSchema, BuildCreateSchema, BuildCreateFromUrlSchema +from ..util import mm_deserialize, get_request_parser +
class BuildListR(Resource):
def get(self):
- parser = reqparse.RequestParser() + parser = get_request_parser()
parser.add_argument('owner', type=str,) parser.add_argument('project_id', type=int) @@ -31,6 +27,7 @@ class BuildListR(Resource): parser.add_argument('limit', type=int) parser.add_argument('offset', type=int)
+ parser.add_argument('state', type=str) # parser.add_argument('package', type=str)
req_args = parser.parse_args() @@ -158,7 +155,7 @@ class BuildListR(Resource): class BuildR(Resource):
def get(self, build_id): - parser = reqparse.RequestParser() + parser = get_request_parser() parser.add_argument('show_chroots', type=bool, default=False) req_args = parser.parse_args()
@@ -171,7 +168,7 @@ class BuildR(Resource): result = render_build(build, self_params) if req_args["show_chroots"]: result["build_chroots"] = [ - render_build_chroot(chroot) + render_build_task(chroot) for chroot in build.build_chroots ]
diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/build_chroot.py b/frontend/coprs_frontend/coprs/rest_api/resources/build_chroot.py deleted file mode 100644 index 3f6da84..0000000 --- a/frontend/coprs_frontend/coprs/rest_api/resources/build_chroot.py +++ /dev/null @@ -1,47 +0,0 @@ -# coding: utf-8 - -from flask import url_for -from flask_restful import Resource - -from coprs.rest_api.common import render_build_chroot -from ...exceptions import MalformedArgumentException -from ...logic.builds_logic import BuildsLogic, BuildChrootsLogic -from ..exceptions import MalformedRequest -from ..util import get_one_safe - - -class BuildChrootListR(Resource): - def get(self, build_id): - build = get_one_safe(BuildsLogic.get(build_id), - "Build {} Not found".format(build_id)) - - return { - "chroots": [ - render_build_chroot(chroot) - for chroot in build.build_chroots - ], - "_links": { - "self": {"href": url_for(".buildchrootlistr", build_id=build_id)} - } - } - - -class BuildChrootR(Resource): - - @staticmethod - def _get_chroot_safe(build_id, name): - try: - chroot = get_one_safe( - BuildChrootsLogic.get_by_build_id_and_name(build_id, name), - "Build chroot {} for build {} not found" - ) - except MalformedArgumentException as err: - raise MalformedRequest("Bad mock chroot name: {}".format(err)) - return chroot - - def get(self, build_id, name): - chroot = self._get_chroot_safe(build_id, name) - return render_build_chroot(chroot) - - # todo: add put method: allows only to pass status: cancelled to cancel build - diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/build_task.py b/frontend/coprs_frontend/coprs/rest_api/resources/build_task.py new file mode 100644 index 0000000..c17a1d1 --- /dev/null +++ b/frontend/coprs_frontend/coprs/rest_api/resources/build_task.py @@ -0,0 +1,111 @@ +# coding: utf-8 + +from flask import url_for +from flask_restful import Resource +from flask_restful import Resource, reqparse +from flask_restful.reqparse import Argument + +from marshmallow import pprint +from marshmallow import Schema, fields +from marshmallow import Schema, fields, validates_schema, ValidationError, validate + +from coprs.helpers import StatusEnum +from coprs.rest_api.common import render_build_task +from ...exceptions import MalformedArgumentException +from ...logic.builds_logic import BuildsLogic, BuildChrootsLogic +from ..exceptions import MalformedRequest +from ..util import get_one_safe, get_request_parser + + +# todo: add redirect from /build_tasks/int:build_id -> /build_tasks?build_id=<build_id> +# class BuildTaskListRedirectBuildIdR(Resource): +# def get(self, build_id): +# resp = make_response("", 302) +# resp.headers["Location"] == url_for(".buildtasklistr", build_id=build_id) + + +class BuildTaskListQuerySchema(Schema): + + build_id = fields.Int() + + +class BuildTaskListR(Resource): + state_choices = StatusEnum.vals.keys() + + def get(self): + + parser = get_request_parser() + + parser.add_argument('owner', type=str,) + parser.add_argument('project_id', type=int) + parser.add_argument('build_id', type=int) + + parser.add_argument('limit', type=int) + parser.add_argument('offset', type=int) + + parser.add_argument( + 'state', type=str, choices=self.state_choices, + help=u"allowed states: {}".format(" ".join(self.state_choices))) + + req_args = parser.parse_args() + + self_params = dict(req_args) + + query = BuildChrootsLogic.get_multiply() + if self_params.get("build_id") is not None: + query = BuildChrootsLogic.filter_by_build_id( + query, self_params["build_id"]) + elif self_params.get("project_id") is not None: + query = BuildChrootsLogic.filter_by_project_id( + query, self_params["project_id"]) + elif self_params.get("owner") is not None: + query = BuildChrootsLogic.filter_by_project_owner_name( + query, self_params["owner"]) + + state = self_params.get("state") + if state: + query = BuildChrootsLogic.filter_by_state(query, state) + + if req_args["limit"] is not None: + limit = req_args["limit"] + if limit <= 0 or limit > 100: + limit = 100 + else: + limit = 100 + self_params["limit"] = limit + query = query.limit(limit) + + if "offset" in self_params is not None: + query = query.offset(self_params["offset"]) + + build_chroots = query.all() + return { + "tasks": [ + render_build_task(chroot) + for chroot in build_chroots + ], + "_links": { + "self": {"href": url_for(".buildtasklistr", **self_params)} + } + } + + +class BuildTaskR(Resource): + + @staticmethod + def _get_chroot_safe(build_id, name): + try: + chroot = get_one_safe( + BuildChrootsLogic.get_by_build_id_and_name(build_id, name), + "Build task {} for build {} not found" + ) + except MalformedArgumentException as err: + raise MalformedRequest("Bad mock chroot name: {}".format(err)) + return chroot + + def get(self, build_id, name): + chroot = self._get_chroot_safe(build_id, name) + return render_build_task(chroot) + + # todo: add put method: allows only to pass status: cancelled to cancel build + diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/mock_chroot.py b/frontend/coprs_frontend/coprs/rest_api/resources/mock_chroot.py index c7135de..a81569c 100644 --- a/frontend/coprs_frontend/coprs/rest_api/resources/mock_chroot.py +++ b/frontend/coprs_frontend/coprs/rest_api/resources/mock_chroot.py @@ -6,7 +6,7 @@ from flask_restful import Resource, reqparse from ...logic.coprs_logic import MockChrootsLogic
from ..schemas import MockChrootSchema -from ..util import get_one_safe +from ..util import get_one_safe, get_request_parser
def render_mock_chroot(chroot): @@ -21,7 +21,7 @@ def render_mock_chroot(chroot): class MockChrootListR(Resource):
def get(self): - parser = reqparse.RequestParser() + parser = get_request_parser() parser.add_argument('active_only', type=bool) req_args = parser.parse_args() active_only = False diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/project.py b/frontend/coprs_frontend/coprs/rest_api/resources/project.py index 148891b..e3157b8 100644 --- a/frontend/coprs_frontend/coprs/rest_api/resources/project.py +++ b/frontend/coprs_frontend/coprs/rest_api/resources/project.py @@ -1,7 +1,4 @@ from logging import getLogger -from coprs.rest_api.exceptions import MalformedRequest - -log = getLogger(__name__)
import flask from flask import url_for, make_response @@ -19,7 +16,9 @@ from ...exceptions import DuplicateException from ..common import rest_api_auth_required, render_copr_chroot, render_build, render_project, get_project_safe from ..schemas import ProjectSchema, ProjectCreateSchema from ..exceptions import ObjectAlreadyExists, CannotProcessRequest, AccessForbidden -from ..util import mm_deserialize +from ..util import mm_deserialize, get_request_parser + +log = getLogger(__name__)
class ProjectListR(Resource): @@ -52,7 +51,7 @@ class ProjectListR(Resource): return resp
def get(self): - parser = reqparse.RequestParser() + parser = get_request_parser() parser.add_argument('owner', type=str) parser.add_argument('name', type=str) parser.add_argument('limit', type=int) @@ -111,7 +110,7 @@ class ProjectR(Resource): return None, 204
def get(self, project_id): - parser = reqparse.RequestParser() + parser = get_request_parser() parser.add_argument('show_builds', type=bool, default=False) parser.add_argument('show_chroots', type=bool, default=False) req_args = parser.parse_args() diff --git a/frontend/coprs_frontend/coprs/rest_api/schemas.py b/frontend/coprs_frontend/coprs/rest_api/schemas.py index cb6f867..b28d1c6 100644 --- a/frontend/coprs_frontend/coprs/rest_api/schemas.py +++ b/frontend/coprs_frontend/coprs/rest_api/schemas.py @@ -128,13 +128,14 @@ class CoprChrootCreateSchema(CoprChrootSchema): name = fields.Str(required=True)
-class BuildChrootSchema(Schema): +class BuildTaskSchema(Schema): # used only for presentation state = fields.Str() started_on = fields.Int(dump_only=True) ended_on = fields.Int(dump_only=True) git_hash = fields.Str(dump_only=True) - name = fields.Str(dump_only=True) + chroot_name = fields.Str(dump_only=True, attribute="name") + build_id = fields.Int(dump_only=True)
result_dir_url = fields.Str(dump_only=True)
diff --git a/frontend/coprs_frontend/coprs/rest_api/util.py b/frontend/coprs_frontend/coprs/rest_api/util.py index d937784..402edd1 100644 --- a/frontend/coprs_frontend/coprs/rest_api/util.py +++ b/frontend/coprs_frontend/coprs/rest_api/util.py @@ -1,10 +1,13 @@ # coding: utf-8 import json
+from flask import Response, url_for, Blueprint import sqlalchemy.orm.exc + +from flask_restful.reqparse import Argument, RequestParser + from .exceptions import ObjectNotFoundError, MalformedRequest -from schemas import AllowedMethodSchema -from flask import Response, url_for, Blueprint +from .schemas import AllowedMethodSchema
class AllowedMethod(object): @@ -62,3 +65,21 @@ def mm_deserialize(schema, json_string):
def mm_serialize_one(schema, obj): return schema().dump(obj)[0] + + +class MyArg(Argument): + def handle_validation_error(self, error, bundle_errors): + # dirty monkey patching, better to switch to webargs + # bundle errors are ignored + data = {u"error": unicode(error)} + if self.help: + data["help"] = self.help + raise MalformedRequest( + "Failed to validate query parameter: {}".format(self.name), + data=data + ) + + +def get_request_parser(): + return RequestParser(argument_class=MyArg) + #return RequestParser(bundle_errors=True) diff --git a/frontend/docs/api_2/source/Resources/build_chroot.rst b/frontend/docs/api_2/source/Resources/build_task.rst similarity index 73% rename from frontend/docs/api_2/source/Resources/build_chroot.rst rename to frontend/docs/api_2/source/Resources/build_task.rst index f767fbe..c5871a4 100644 --- a/frontend/docs/api_2/source/Resources/build_chroot.rst +++ b/frontend/docs/api_2/source/Resources/build_task.rst @@ -1,15 +1,16 @@ -Build Chroot -============ +Build Task +==========
-Build chroot represents information about individual build tasks for each chroot. +Build task represents information about individual build tasks per each chroot.
-Structure of the build chroot entity ------------------------------------- +Structure of the build task entity +----------------------------------
.. code-block:: javascript
{ "name": "fedora-rawhide-x86_64", + "build_id": 12345, "started_on": 1440753865, "ended_on": 1440753919, "state": "succeeded", @@ -22,7 +23,8 @@ Build chroot fields ================== ==================== =============== Field Type Description ================== ==================== =============== -name str chroot name +chroot_name str chroot name +build_id int unique build identifier state str current build state started_on int(unixtime UTC) time when the build chroot started ended_on int(unixtime UTC) time when the build chroot ended @@ -38,11 +40,24 @@ result_dir_url str(URL) location of the build results List build chroots ------------------
-.. http:get:: /api_2/builds/(int:build_id)/chroots +.. http:get:: /api_2/builds_tasks
- Returns list of build chroots contained in the one build + Returns list of build tasks according to the given query parameters
- :param int build_id: a unique identifier of the build + :query str owner: select build tasks from projects owned by this user + :query int project_id: + select build tasks from one project, + when used query parameter ``owner`` is ignored + :query int build_id: + select build tasks from one project, + when used query parameters ``owner`` and ``project_id`` are ignored + + :query int offset: offset number, default value is 0 + :query int limit: limit number, default value is 100 + :query str state: + select builds in particular state, allowed values: + ``failed``, ``succeeded``, ``canceled``, ``running``, + ``pending``, ``starting``, ``importing``
:statuscode 200: no error :statuscode 404: build not found @@ -51,7 +66,7 @@ List build chroots
.. sourcecode:: http
- GET /api_2/builds/106882/chroots HTTP/1.1 + GET /api_2/builds_tasks?build_id=106882 HTTP/1.1 Host: copr.fedoraproject.org
**Response** @@ -91,18 +106,18 @@ List build chroots
-Get build chroot details ------------------------- +Get build task details +----------------------
.. http:get:: /api_2/builds/(int:build_id)/chroots/(str:name)
- Returns details about one build chroot + Returns details about one build task
:param int build_id: a unique identifier of the build :param str name: chroot name
:statuscode 200: no error - :statuscode 404: build or build chroot not found + :statuscode 404: build or build task not found
**Example request**
diff --git a/frontend/docs/api_2/source/index.rst b/frontend/docs/api_2/source/index.rst index d7dfcba..3ed6a68 100644 --- a/frontend/docs/api_2/source/index.rst +++ b/frontend/docs/api_2/source/index.rst @@ -22,7 +22,7 @@ Resources Resources/project Resources/project_chroot Resources/build - Resources/build_chroot + Resources/build_task Resources/mock_chroot