From ea384e7ee315003e0e8d30f3645d47d65abf64c5 Mon Sep 17 00:00:00 2001 From: Qixiang Wan Date: Jun 13 2017 07:28:35 +0000 Subject: [PATCH 1/4] Import views to register apis --- diff --git a/freshmaker/__init__.py b/freshmaker/__init__.py index 6e118e9..ca709c8 100644 --- a/freshmaker/__init__.py +++ b/freshmaker/__init__.py @@ -40,3 +40,5 @@ db = SQLAlchemy(app) conf = init_config(app) init_logging(conf) log = getLogger(__name__) + +from freshmaker import views # noqa From cf8c432b026680ec636f9be643869f467522a08b Mon Sep 17 00:00:00 2001 From: Qixiang Wan Date: Jun 13 2017 07:28:35 +0000 Subject: [PATCH 2/4] Add artifact build query api --- diff --git a/freshmaker/api_utils.py b/freshmaker/api_utils.py new file mode 100644 index 0000000..1692457 --- /dev/null +++ b/freshmaker/api_utils.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from flask import request, url_for + +from freshmaker.types import ArtifactType, ArtifactBuildState +from freshmaker.models import ArtifactBuild + + +def pagination_metadata(p_query): + """ + Returns a dictionary containing metadata about the paginated query. This must be run as part of a Flask request. + :param p_query: flask_sqlalchemy.Pagination object + :return: a dictionary containing metadata about the paginated query + """ + + pagination_data = { + 'page': p_query.page, + 'per_page': p_query.per_page, + 'total': p_query.total, + 'pages': p_query.pages, + 'first': url_for(request.endpoint, page=1, per_page=p_query.per_page, _external=True), + 'last': url_for(request.endpoint, page=p_query.pages, per_page=p_query.per_page, _external=True) + } + + if p_query.has_prev: + pagination_data['prev'] = url_for(request.endpoint, page=p_query.prev_num, + per_page=p_query.per_page, _external=True) + if p_query.has_next: + pagination_data['next'] = url_for(request.endpoint, page=p_query.next_num, + per_page=p_query.per_page, _external=True) + + return pagination_data + + +def filter_artifact_builds(flask_request): + """ + Returns a flask_sqlalchemy.Pagination object based on the request parameters + :param request: Flask request object + :return: flask_sqlalchemy.Pagination + """ + search_query = dict() + + artifact_type = flask_request.args.get('type', None) + if artifact_type: + if artifact_type.isdigit(): + if int(artifact_type) in [t.value for t in list(ArtifactType)]: + search_query['type'] = artifact_type + else: + raise ValueError('An invalid artifact type was supplied') + else: + if str(artifact_type).upper() in [t.name for t in list(ArtifactType)]: + search_query['type'] = ArtifactType[artifact_type.upper()].value + else: + raise ValueError('An invalid artifact type was supplied') + + state = flask_request.args.get('state', None) + if state: + if state.isdigit(): + if int(state) in [s.value for s in list(ArtifactBuildState)]: + search_query['state'] = state + else: + raise ValueError('An invalid state was supplied') + else: + if str(state).upper() in [s.name for s in list(ArtifactBuildState)]: + search_query['state'] = ArtifactBuildState[state.upper()].value + else: + raise ValueError('An invalid state was supplied') + + for key in ['name', 'event_id', 'dep_of_id', 'build_id']: + if flask_request.args.get(key, None): + search_query[key] = flask_request.args[key] + + query = ArtifactBuild.query + + if search_query: + query = query.filter_by(**search_query) + + page = flask_request.args.get('page', 1, type=int) + per_page = flask_request.args.get('per_page', 10, type=int) + return query.paginate(page, per_page, False) diff --git a/freshmaker/models.py b/freshmaker/models.py index cf0d207..99fff4b 100644 --- a/freshmaker/models.py +++ b/freshmaker/models.py @@ -150,6 +150,18 @@ class ArtifactBuild(FreshmakerBase): self.name, ArtifactType(self.type).name, ArtifactBuildState(self.state).name, self.event.message_id) + def json(self): + return { + "id": self.id, + "name": self.name, + "type": self.type, + "state": self.state, + "time_submitted": self.time_submitted, + "time_completed": self.time_completed, + "event_id": self.event_id, + "build_id": self.build_id, + } + def get_root_dep_of(self): dep_of = self.dep_of while dep_of: diff --git a/freshmaker/views.py b/freshmaker/views.py index 2e995de..d70e3c1 100644 --- a/freshmaker/views.py +++ b/freshmaker/views.py @@ -21,35 +21,82 @@ # # Written by Jan Kaluza +from flask import request, jsonify from flask.views import MethodView from freshmaker import app +from freshmaker.api_utils import pagination_metadata, filter_artifact_builds +from freshmaker.models import ArtifactBuild api_v1 = { - 'freshmaker': { - 'url': '/freshmaker/1/events/', - 'options': { - 'defaults': {'id': None}, - 'methods': ['GET'], - } + 'events': { + 'events_list': { + 'url': '/freshmaker/1/events/', + 'options': { + 'defaults': {'id': None}, + 'methods': ['GET'], + } + }, + }, + 'builds': { + 'builds_list': { + 'url': '/freshmaker/1/builds/', + 'options': { + 'defaults': {'id': None}, + 'methods': ['GET'], + } + }, + 'build': { + 'url': '/freshmaker/1/builds/', + 'options': { + 'methods': ['GET'], + } + }, }, } -class FreshmakerAPI(MethodView): +class EventAPI(MethodView): def get(self, id): return "Done", 200 +class BuildAPI(MethodView): + def get(self, id): + if id is None: + p_query = filter_artifact_builds(request) + + json_data = { + 'meta': pagination_metadata(p_query) + } + json_data['items'] = [item.json() for item in p_query.items] + + return jsonify(json_data), 200 + + else: + build = ArtifactBuild.query.filter_by(id=id).first() + if build: + return jsonify(build.json()), 200 + else: + raise ValueError('No such build found.') + + +API_V1_MAPPING = { + 'events': EventAPI, + 'builds': BuildAPI, +} + + def register_api_v1(): """ Registers version 1 of MBS API. """ - module_view = FreshmakerAPI.as_view('freshmaker') - for key, val in api_v1.items(): - app.add_url_rule(val['url'], - endpoint=key, - view_func=module_view, - **val['options']) + for k, v in API_V1_MAPPING.items(): + view = v.as_view(k) + for key, val in api_v1.get(k, {}).items(): + app.add_url_rule(val['url'], + endpoint=key, + view_func=view, + **val['options']) register_api_v1() diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 0000000..6c7780c --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +import json + +from freshmaker import app, db, events, models +from freshmaker.types import ArtifactType, ArtifactBuildState + + +class TestViews(unittest.TestCase): + def setUp(self): + db.session.remove() + db.drop_all() + db.create_all() + db.session.commit() + + self._init_data() + + self.client = app.test_client() + + def _init_data(self): + event = models.Event.create(db.session, "2017-00000000-0000-0000-0000-000000000001", "RHSA-2018-101", events.TestingEvent) + models.ArtifactBuild.create(db.session, event, "ed", "module", 1234) + models.ArtifactBuild.create(db.session, event, "mksh", "module", 1235) + models.ArtifactBuild.create(db.session, event, "bash", "module", 1236) + db.session.commit() + db.session.expire_all() + + def test_query_build(self): + resp = self.client.get('/freshmaker/1/builds/1') + data = json.loads(resp.data.decode('utf8')) + self.assertEqual(data['id'], 1) + self.assertEqual(data['name'], 'ed') + self.assertEqual(data['type'], ArtifactType.MODULE.value) + self.assertEqual(data['state'], ArtifactBuildState.BUILD.value) + self.assertEqual(data['event_id'], 1) + self.assertEqual(data['build_id'], 1234) + + def test_query_builds(self): + resp = self.client.get('/freshmaker/1/builds/') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 3) + for name in ['ed', 'mksh', 'bash']: + self.assertIn(name, [b['name'] for b in builds]) + for build_id in [1234, 1235, 1236]: + self.assertIn(build_id, [b['build_id'] for b in builds]) + + def test_query_builds_by_name(self): + resp = self.client.get('/freshmaker/1/builds/?name=ed') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 1) + self.assertEqual(builds[0]['name'], 'ed') + + resp = self.client.get('/freshmaker/1/builds/?name=mksh') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 1) + self.assertEqual(builds[0]['name'], 'mksh') + + resp = self.client.get('/freshmaker/1/builds/?name=nonexist') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 0) + + def test_query_builds_by_type(self): + resp = self.client.get('/freshmaker/1/builds/?type=0') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 0) + + resp = self.client.get('/freshmaker/1/builds/?type=1') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 0) + + resp = self.client.get('/freshmaker/1/builds/?type=2') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 3) + + resp = self.client.get('/freshmaker/1/builds/?type=module') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 3) + + def test_query_builds_by_invalid_type(self): + with self.assertRaises(ValueError) as ctx: + self.client.get('/freshmaker/1/builds/?type=100') + self.assertEqual(str(ctx.exception), 'An invalid artifact type was supplied') + + def test_query_builds_by_state(self): + resp = self.client.get('/freshmaker/1/builds/?state=0') + builds = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(builds), 3) + + def test_query_builds_by_invalid_state(self): + with self.assertRaises(ValueError) as ctx: + self.client.get('/freshmaker/1/builds/?state=100') + self.assertEqual(str(ctx.exception), 'An invalid state was supplied') + + +if __name__ == '__main__': + unittest.main() From d8980a14ca5fea36e6088c147c225a0766e2ea30 Mon Sep 17 00:00:00 2001 From: Qixiang Wan Date: Jun 13 2017 07:28:35 +0000 Subject: [PATCH 3/4] Add event query api --- diff --git a/freshmaker/api_utils.py b/freshmaker/api_utils.py index 1692457..bfc1cbd 100644 --- a/freshmaker/api_utils.py +++ b/freshmaker/api_utils.py @@ -22,7 +22,7 @@ from flask import request, url_for from freshmaker.types import ArtifactType, ArtifactBuildState -from freshmaker.models import ArtifactBuild +from freshmaker.models import ArtifactBuild, Event def pagination_metadata(p_query): @@ -97,3 +97,25 @@ def filter_artifact_builds(flask_request): page = flask_request.args.get('page', 1, type=int) per_page = flask_request.args.get('per_page', 10, type=int) return query.paginate(page, per_page, False) + + +def filter_events(flask_request): + """ + Returns a flask_sqlalchemy.Pagination object based on the request parameters + :param request: Flask request object + :return: flask_sqlalchemy.Pagination + """ + search_query = dict() + + for key in ['message_id', 'search_key', 'event_type_id']: + if flask_request.args.get(key, None): + search_query[key] = flask_request.args[key] + + query = Event.query + + if search_query: + query = query.filter_by(**search_query) + + page = flask_request.args.get('page', 1, type=int) + per_page = flask_request.args.get('per_page', 10, type=int) + return query.paginate(page, per_page, False) diff --git a/freshmaker/models.py b/freshmaker/models.py index 99fff4b..df6516f 100644 --- a/freshmaker/models.py +++ b/freshmaker/models.py @@ -92,6 +92,15 @@ class Event(FreshmakerBase): def __repr__(self): return "" % (self.message_id, self.event_type, self.search_key) + def json(self): + return { + "id": self.id, + "message_id": self.message_id, + "search_key": self.search_key, + "event_type_id": self.event_type_id, + "builds": [b.id for b in self.builds], + } + class ArtifactBuild(FreshmakerBase): __tablename__ = "artifact_builds" diff --git a/freshmaker/views.py b/freshmaker/views.py index d70e3c1..bbb882d 100644 --- a/freshmaker/views.py +++ b/freshmaker/views.py @@ -25,8 +25,8 @@ from flask import request, jsonify from flask.views import MethodView from freshmaker import app -from freshmaker.api_utils import pagination_metadata, filter_artifact_builds -from freshmaker.models import ArtifactBuild +from freshmaker import models +from freshmaker.api_utils import pagination_metadata, filter_artifact_builds, filter_events api_v1 = { 'events': { @@ -37,6 +37,12 @@ api_v1 = { 'methods': ['GET'], } }, + 'event': { + 'url': '/freshmaker/1/events/', + 'options': { + 'methods': ['GET'], + } + }, }, 'builds': { 'builds_list': { @@ -59,7 +65,22 @@ api_v1 = { class EventAPI(MethodView): def get(self, id): - return "Done", 200 + if id is None: + p_query = filter_events(request) + + json_data = { + 'meta': pagination_metadata(p_query) + } + json_data['items'] = [item.json() for item in p_query.items] + + return jsonify(json_data), 200 + + else: + event = models.Event.query.filter_by(id=id).first() + if event: + return jsonify(event.json()), 200 + else: + raise ValueError('No shuch event found.') class BuildAPI(MethodView): @@ -75,7 +96,7 @@ class BuildAPI(MethodView): return jsonify(json_data), 200 else: - build = ArtifactBuild.query.filter_by(id=id).first() + build = models.ArtifactBuild.query.filter_by(id=id).first() if build: return jsonify(build.json()), 200 else: diff --git a/tests/test_views.py b/tests/test_views.py index 6c7780c..1d74285 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -42,6 +42,7 @@ class TestViews(unittest.TestCase): models.ArtifactBuild.create(db.session, event, "ed", "module", 1234) models.ArtifactBuild.create(db.session, event, "mksh", "module", 1235) models.ArtifactBuild.create(db.session, event, "bash", "module", 1236) + models.Event.create(db.session, "2017-00000000-0000-0000-0000-000000000002", "RHSA-2018-102", events.TestingEvent) db.session.commit() db.session.expire_all() @@ -111,6 +112,32 @@ class TestViews(unittest.TestCase): self.client.get('/freshmaker/1/builds/?state=100') self.assertEqual(str(ctx.exception), 'An invalid state was supplied') + def test_query_event(self): + resp = self.client.get('/freshmaker/1/events/1') + data = json.loads(resp.data.decode('utf8')) + self.assertEqual(data['id'], 1) + self.assertEqual(data['message_id'], '2017-00000000-0000-0000-0000-000000000001') + self.assertEqual(data['search_key'], 'RHSA-2018-101') + self.assertEqual(data['event_type_id'], models.EVENT_TYPES[events.TestingEvent]) + self.assertEqual(data['builds'], [1, 2, 3]) + + def test_query_events(self): + resp = self.client.get('/freshmaker/1/events/') + evs = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(evs), 2) + + def test_query_event_by_message_id(self): + resp = self.client.get('/freshmaker/1/events/?message_id=2017-00000000-0000-0000-0000-000000000001') + evs = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(evs), 1) + self.assertEqual(evs[0]['message_id'], '2017-00000000-0000-0000-0000-000000000001') + + def test_query_event_by_search_key(self): + resp = self.client.get('/freshmaker/1/events/?search_key=RHSA-2018-101') + evs = json.loads(resp.data.decode('utf8'))['items'] + self.assertEqual(len(evs), 1) + self.assertEqual(evs[0]['search_key'], 'RHSA-2018-101') + if __name__ == '__main__': unittest.main() From c230cb1519913bd0e7fca32467379310cb252ab8 Mon Sep 17 00:00:00 2001 From: Qixiang Wan Date: Jun 13 2017 07:28:35 +0000 Subject: [PATCH 4/4] Workaround test warning of cannot collect tests from TestingEvent Workaround the test warning of: tests/test_models.py::TestingEvent cannot collect test class 'TestingEvent' because it has a __init__ constructor --- diff --git a/tests/test_models.py b/tests/test_models.py index c45113b..853a188 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -22,9 +22,8 @@ import unittest -from freshmaker import db +from freshmaker import db, events from freshmaker.models import Event, ArtifactBuild -from freshmaker.events import TestingEvent class TestModels(unittest.TestCase): @@ -40,7 +39,7 @@ class TestModels(unittest.TestCase): db.session.commit() def test_creating_event_and_builds(self): - event = Event.create(db.session, "test_msg_id", "RHSA-2017-284", TestingEvent) + event = Event.create(db.session, "test_msg_id", "RHSA-2017-284", events.TestingEvent) build = ArtifactBuild.create(db.session, event, "ed", "module", 1234) ArtifactBuild.create(db.session, event, "mksh", "module", 1235, build) db.session.commit() @@ -49,7 +48,7 @@ class TestModels(unittest.TestCase): e = db.session.query(Event).filter(event.id == 1).one() self.assertEqual(e.message_id, "test_msg_id") self.assertEqual(e.search_key, "RHSA-2017-284") - self.assertEqual(e.event_type, TestingEvent) + self.assertEqual(e.event_type, events.TestingEvent) self.assertEqual(len(e.builds), 2) self.assertEqual(e.builds[0].name, "ed") @@ -65,7 +64,7 @@ class TestModels(unittest.TestCase): self.assertEqual(e.builds[1].dep_of.name, "ed") def test_get_root_dep_of(self): - event = Event.create(db.session, "test_msg_id", "test", TestingEvent) + event = Event.create(db.session, "test_msg_id", "test", events.TestingEvent) build1 = ArtifactBuild.create(db.session, event, "ed", "module", 1234) build2 = ArtifactBuild.create(db.session, event, "mksh", "module", 1235, build1) build3 = ArtifactBuild.create(db.session, event, "runtime", "module", 1236, build2)