#53 Add query APIs for querying artifact builds and events
Merged 8 years ago by qwan. Opened 8 years ago by qwan.
qwan/freshmaker add-query-apis  into  master

file modified
+2
@@ -40,3 +40,5 @@ 

  conf = init_config(app)

  init_logging(conf)

  log = getLogger(__name__)

+ 

+ from freshmaker import views  # noqa

@@ -0,0 +1,121 @@ 

+ # -*- 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, Event

+ 

+ 

+ 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)

+ 

+ 

+ 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)

file modified
+21
@@ -92,6 +92,15 @@ 

      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"
@@ -150,6 +159,18 @@ 

              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:

file modified
+82 -14
@@ -21,35 +21,103 @@ 

  #

  # Written by Jan Kaluza <jkaluza@redhat.com>

  

+ from flask import request, jsonify

  from flask.views import MethodView

  

  from freshmaker import app

+ from freshmaker import models

+ from freshmaker.api_utils import pagination_metadata, filter_artifact_builds, filter_events

  

  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'],

+             }

+         },

+         'event': {

+             'url': '/freshmaker/1/events/<int:id>',

+             'options': {

+                 '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):

+         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):

      def get(self, id):

-         return "Done", 200

+         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 = models.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()

file modified
+4 -5
@@ -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 @@ 

          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 @@ 

          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 @@ 

          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)

file added
+143
@@ -0,0 +1,143 @@ 

+ # -*- 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)

+         models.Event.create(db.session, "2017-00000000-0000-0000-0000-000000000002", "RHSA-2018-102", events.TestingEvent)

+         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')

+ 

+     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()
cqi commented 8 years ago

We use pytest to run tests. Is this necessary?

qwan commented 8 years ago

It doesn't hurt to have :) This makes it easy to only run view tests by python test_views.py when I make some changes to this file.

no initial comment

We use pytest to run tests. Is this necessary?

It doesn't hurt to have :) This makes it easy to only run view tests by python test_views.py when I make some changes to this file.

Pull-Request has been merged by qwan

8 years ago