[LNT] r242070 - Start adding a simple REST API to LNT

Chris Matthews cmatthews5 at apple.com
Mon Jul 13 14:36:32 PDT 2015


Author: cmatthews
Date: Mon Jul 13 16:36:32 2015
New Revision: 242070

URL: http://llvm.org/viewvc/llvm-project?rev=242070&view=rev
Log:
Start adding a simple REST API to LNT

Add a small RESTful API for new clients (including ajax fronted).
FlaskRESTful is added as a dependency, as it is easy way of building
complex REST APIs.  This patch adds enough API to list machines, orders
and runs.  Machines can be enumerated, and runs for each machines can
be explored.  Sample data and FieldChanges will be in a separate patch.

Added:
    lnt/trunk/lnt/server/ui/api.py
    lnt/trunk/tests/server/ui/test_api.py
Modified:
    lnt/trunk/lnt/server/ui/app.py
    lnt/trunk/setup.py
    lnt/trunk/tests/server/ui/V4Pages.py

Added: lnt/trunk/lnt/server/ui/api.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/api.py?rev=242070&view=auto
==============================================================================
--- lnt/trunk/lnt/server/ui/api.py (added)
+++ lnt/trunk/lnt/server/ui/api.py Mon Jul 13 16:36:32 2015
@@ -0,0 +1,161 @@
+from flask import current_app, g
+from flask import request
+from sqlalchemy.orm.exc import NoResultFound
+from flask_restful import Resource, reqparse, fields, marshal_with, abort
+
+parser = reqparse.RequestParser()
+parser.add_argument('db', type=str)
+
+
+def in_db(func):
+    """Extract the database information off the request and attach to
+    particular test suite and database."""
+    def wrap(*args, **kwargs):
+        db = kwargs.pop('db')
+        ts = kwargs.pop('ts')
+        g.db_name = db
+        g.testsuite_name = ts
+        g.db_info = current_app.old_config.databases.get(g.db_name)
+        if g.db_info is None:
+            abort(404, message="Invalid database.")
+        # Compute result.
+        result = func(*args, **kwargs)
+
+        # Make sure that any transactions begun by this request are finished.
+        request.get_db().rollback()
+        return result
+    return wrap
+
+
+def ts_path(path):
+    """Make a URL path with a database and test suite embedded in them."""
+    return "/api/db_<string:db>/v4/<string:ts>/" + path
+
+
+def with_ts(obj):
+    """For Url type fields to work, the objects we return must have a test-suite
+    and database attribute set, the function attempts to set them."""
+    if type(obj) == list:
+        # For lists, set them on all elements.
+        return [with_ts(x) for x in obj]
+    if type(obj) == dict:
+        # If already a dict, just add the fields.
+        new_obj = obj
+    else:
+        # Sqlalcamey objects are read-only and store their attributes in a
+        # sub-dict.  Make a copy so we can edit it.
+        new_obj = obj.__dict__.copy()
+
+    new_obj['db'] = g.db_name
+    new_obj['ts'] = g.testsuite_name
+    return new_obj
+
+
+# This date format is what the JavaScript in the LNT frontend likes.
+DATE_FORMAT = "iso8601"
+
+
+machines_fields = {
+    'id': fields.Integer,
+    'name': fields.String,
+    'os': fields.String,
+    'hardware': fields.String,
+}
+
+
+class Machines(Resource):
+    """List all the machines and give summary information."""
+    method_decorators = [in_db]
+
+    @marshal_with(machines_fields)
+    def get(self):
+        ts = request.get_testsuite()
+        changes = ts.query(ts.Machine).all()
+        return changes
+
+
+machine_fields = {
+    'id': fields.Integer,
+    'name': fields.String,
+    'os': fields.String,
+    'hardware': fields.String,
+    'runs': fields.List(fields.Url('runs')),
+}
+
+
+class Machine(Resource):
+    """Detailed results about a particular machine, including runs on it."""
+    method_decorators = [in_db]
+
+    @marshal_with(machine_fields)
+    def get(self, machine_id):
+
+        ts = request.get_testsuite()
+        try:
+            machine = ts.query(ts.Machine).filter(
+                    ts.Machine.id == machine_id).one()
+        except NoResultFound:
+            abort(404, message="Invalid machine.")
+
+        machine = with_ts(machine)
+        machine_runs = ts.query(ts.Run.id).join(ts.Machine).filter(
+                ts.Machine.id == machine_id).all()
+
+        machine['runs'] = with_ts([dict(run_id=x[0]) for x in machine_runs])
+        print machine['runs']
+        return machine
+
+
+run_fields = {
+    'id': fields.Integer,
+    'start_time': fields.DateTime(dt_format=DATE_FORMAT),
+    'end_time': fields.DateTime(dt_format=DATE_FORMAT),
+    'machine_id': fields.Integer,
+    'machine': fields.Url("machine"),
+    'order_id': fields.Integer,
+    'order': fields.Url("order"),
+}
+
+
+class Runs(Resource):
+    method_decorators = [in_db]
+
+    @marshal_with(run_fields)
+    def get(self, run_id):
+        ts = request.get_testsuite()
+        try:
+            changes = ts.query(ts.Run).join(ts.Machine).filter(
+                ts.Run.id == run_id).one()
+        except NoResultFound:
+            abort(404, message="Invalid run.")
+
+        changes = with_ts(changes)
+        return changes
+
+
+order_fields = {
+    'id': fields.Integer,
+    'llvm_project_revision': fields.String,
+    'next_order_id': fields.Integer,
+    'previous_order_id': fields.Integer,
+}
+
+
+class Order(Resource):
+    method_decorators = [in_db]
+
+    @marshal_with(order_fields)
+    def get(self, order_id):
+        ts = request.get_testsuite()
+        try:
+            changes = ts.query(ts.Order).filter(ts.Order.id == order_id).one()
+        except NoResultFound:
+            abort(404, message="Invalid order.")
+        return changes
+
+
+def load_api_resources(api):
+    api.add_resource(Machines, ts_path("machines"))
+    api.add_resource(Machine, ts_path("machine/<int:machine_id>"))
+    api.add_resource(Runs, ts_path("run/<int:run_id>"))
+    api.add_resource(Order, ts_path("order/<int:order_id>"))

Modified: lnt/trunk/lnt/server/ui/app.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/app.py?rev=242070&r1=242069&r2=242070&view=diff
==============================================================================
--- lnt/trunk/lnt/server/ui/app.py (original)
+++ lnt/trunk/lnt/server/ui/app.py Mon Jul 13 16:36:32 2015
@@ -10,6 +10,8 @@ import flask
 from flask import current_app
 from flask import g
 from flask import url_for
+from flask import Flask
+from flask_restful import Resource, Api
 
 import lnt
 import lnt.server.db.v4db
@@ -17,6 +19,8 @@ import lnt.server.instance
 import lnt.server.ui.filters
 import lnt.server.ui.globals
 import lnt.server.ui.views
+from lnt.server.ui.api import load_api_resources
+
 
 class RootSlashPatchMiddleware(object):
     def __init__(self, app):
@@ -98,7 +102,11 @@ class App(flask.Flask):
 
         # Load the application routes.
         app.register_module(lnt.server.ui.views.frontend)
-                        
+
+        # Load the flaskRESTful API.
+        app.api = Api(app)
+        load_api_resources(app.api)
+
         return app
 
     @staticmethod

Modified: lnt/trunk/setup.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/setup.py?rev=242070&r1=242069&r2=242070&view=diff
==============================================================================
--- lnt/trunk/setup.py (original)
+++ lnt/trunk/setup.py Mon Jul 13 16:36:32 2015
@@ -90,5 +90,5 @@ http://llvm.org/svn/llvm-project/lnt/tru
             'lnt = lnt.lnttool:main',
             ],
         },
-    install_requires=['SQLAlchemy', 'Flask'],
+    install_requires=['SQLAlchemy', 'Flask', 'flask-restful'],
 )

Modified: lnt/trunk/tests/server/ui/V4Pages.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/tests/server/ui/V4Pages.py?rev=242070&r1=242069&r2=242070&view=diff
==============================================================================
--- lnt/trunk/tests/server/ui/V4Pages.py (original)
+++ lnt/trunk/tests/server/ui/V4Pages.py Mon Jul 13 16:36:32 2015
@@ -15,16 +15,26 @@ from htmlentitydefs import name2codepoin
 
 import lnt.server.db.migrate
 import lnt.server.ui.app
+import json
 
 logging.basicConfig(level=logging.DEBUG)
 
 
-def check_code(client, url, expected_code=200):
-    resp = client.get(url, follow_redirects=False)
+def check_code(client, url, expected_code=200, data_to_send=None):
+    """Call a flask url, and make sure the return code is good."""
+    resp = client.get(url, follow_redirects=False, data=data_to_send)
     assert resp.status_code == expected_code, \
         "Call to %s returned: %d, not the expected %d"%(url, resp.status_code, expected_code)
     return resp
 
+
+def check_json(client, url, expected_code=200, data_to_send=None):
+    """Call a flask url, make sure the return code is good,
+    and grab reply data from the json payload."""
+    return json.loads(check_code(client, url, expected_code,
+                      data_to_send=data_to_send).data)
+
+
 def check_redirect(client, url, expected_redirect_regex):
     resp = client.get(url, follow_redirects=False)
     assert resp.status_code == 302, \

Added: lnt/trunk/tests/server/ui/test_api.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/tests/server/ui/test_api.py?rev=242070&view=auto
==============================================================================
--- lnt/trunk/tests/server/ui/test_api.py (added)
+++ lnt/trunk/tests/server/ui/test_api.py Mon Jul 13 16:36:32 2015
@@ -0,0 +1,100 @@
+# Check that the LNT REST JSON API is working.
+# create temporary instance
+# RUN: rm -rf %t.instance
+# RUN: python %{shared_inputs}/create_temp_instance.py \
+# RUN:     %{shared_inputs}/SmallInstance \
+# RUN:     %t.instance %S/Inputs/V4Pages_extra_records.sql
+#
+# RUN: python %s %t.instance
+
+import unittest
+import logging
+import sys
+
+import lnt.server.db.migrate
+import lnt.server.ui.app
+
+from V4Pages import check_json
+logging.basicConfig(level=logging.DEBUG)
+
+machines_expected_response = [{u'hardware': u'x86_64',
+                               u'os': u'Darwin 11.3.0',
+                               u'id': 1,
+                               u'name': u'localhost__clang_DEV__x86_64'},
+                              {u'hardware': u'AArch64',
+                               u'os': u'linux',
+                               u'id': 2,
+                               u'name': u'machine2'},
+                              {u'hardware': u'AArch64',
+                               u'os': u'linux',
+                               u'id': 3,
+                               u'name': u'machine3'}]
+
+# Machine add some extra fields, so add them.
+machine_expected_response = list(machines_expected_response)
+machine_expected_response[0] = machines_expected_response[0].copy()
+machine_expected_response[0][u'runs'] = [u'/api/db_default/v4/nts/run/1',
+                                         u'/api/db_default/v4/nts/run/2']
+
+machine_expected_response[1] = machines_expected_response[1].copy()
+machine_expected_response[1][u'runs'] = [u'/api/db_default/v4/nts/run/3']
+
+machine_expected_response[2] = machines_expected_response[2].copy()
+machine_expected_response[2][u'runs'] = [u'/api/db_default/v4/nts/run/4']
+
+
+run_expected_response = [{u'end_time': u'2012-04-11T16:28:58',
+                          u'id': 1,
+                          u'machine_id': 1,
+                          u'machine': u'/api/db_default/v4/nts/machine/1',
+                          u'order_id': 3,
+                          u'order': u'/api/db_default/v4/nts/order/3',
+                          u'start_time': u'2012-04-11T16:28:23'}]
+
+order_expected_response = {u'id': 3,
+                           u'llvm_project_revision': "154331",
+                           u'next_order_id': 0,
+                           u'previous_order_id': 4}
+
+
+class JSONAPITester(unittest.TestCase):
+    """Test the REST api."""
+
+    def setUp(self):
+        """Bind to the LNT test instance."""
+        _, instance_path = sys.argv
+        app = lnt.server.ui.app.App.create_standalone(instance_path)
+        app.testing = True
+        self.client = app.test_client()
+
+    def test_machine_api(self):
+        """Check /machines and /machine/n return expected results from
+        testdb.
+        """
+        client = self.client
+        j = check_json(client, 'api/db_default/v4/nts/machines')
+        self.assertEquals(j, machines_expected_response)
+        for i in xrange(0, len(machine_expected_response)):
+            j = check_json(client, 'api/db_default/v4/nts/machine/' +
+                           str(i + 1))
+            self.assertEquals(j, machine_expected_response[i])
+
+    def test_run_api(self):
+        """Check /run/n returns expected run information."""
+        client = self.client
+        j = check_json(client, 'api/db_default/v4/nts/run/1')
+        self.assertEquals(j, run_expected_response[0])
+
+        for i in xrange(0, len(run_expected_response)):
+            j = check_json(client, 'api/db_default/v4/nts/run/' + str(i + 1))
+            self.assertEquals(j, run_expected_response[i])
+
+    def test_order_api(self):
+        """ Check /order/n returns the expected order information."""
+        client = self.client
+        j = check_json(client, 'api/db_default/v4/nts/order/3')
+        self.assertEquals(j, order_expected_response)
+
+
+if __name__ == '__main__':
+    unittest.main(argv=[sys.argv[0], ])





More information about the llvm-commits mailing list