From ea384e7ee315003e0e8d30f3645d47d65abf64c5 Mon Sep 17 00:00:00 2001
From: Qixiang Wan <qwan@redhat.com>
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 <qwan@redhat.com>
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 <jkaluza@redhat.com>
 
+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/<int:id>',
+            '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 <qwan@redhat.com>
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 "<Event %s, %r, %s>" % (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/<int:id>',
+            '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 <qwan@redhat.com>
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)

