aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Johannes Christ <[email protected]>2018-06-05 15:14:52 +0200
committerGravatar Gareth Coles <[email protected]>2018-06-05 14:14:52 +0100
commitb8c2be0b446be35c273ccaf5ff10fd980be0edcc (patch)
treec1a068d3e43420e26626c2fd53e1e8c06da6c24c
parent[Jams] Fix signup message (diff)
documentation metadata API (#57)
* Add documentation metadata "schema". * Add seed data for `pydoc_links` table. * Add tests for the `doc` API. * Allow specifying multiple parameters. * Move up line in generator. * make each docs test a function for greater test report visibility * fix a typo * Use fixtures instead of `pytest-ordering`. * Move `doc` API to `/bot/doc`. * Use new migration system.
-rw-r--r--pysite/migrations/tables/pydoc_links/__init__.py0
-rw-r--r--pysite/migrations/tables/pydoc_links/initial_data.json22
-rw-r--r--pysite/tables.py12
-rw-r--r--pysite/views/api/bot/doc.py98
-rw-r--r--tests/test_api.py2
-rw-r--r--tests/test_api_docs.py95
6 files changed, 227 insertions, 2 deletions
diff --git a/pysite/migrations/tables/pydoc_links/__init__.py b/pysite/migrations/tables/pydoc_links/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pysite/migrations/tables/pydoc_links/__init__.py
diff --git a/pysite/migrations/tables/pydoc_links/initial_data.json b/pysite/migrations/tables/pydoc_links/initial_data.json
new file mode 100644
index 00000000..e5b21357
--- /dev/null
+++ b/pysite/migrations/tables/pydoc_links/initial_data.json
@@ -0,0 +1,22 @@
+[
+ {
+ "package": "aiohttp",
+ "base_url": "https://aiohttp.readthedocs.io/en/stable/",
+ "inventory_url": "https://aiohttp.readthedocs.io/en/stable/objects.inv"
+ },
+ {
+ "package": "discord",
+ "base_url": "https://discordpy.readthedocs.io/en/rewrite/",
+ "inventory_url": "https://discordpy.readthedocs.io/en/rewrite/objects.inv"
+ },
+ {
+ "package": "django",
+ "base_url": "https://docs.djangoproject.com/en/dev/",
+ "inventory_url": "https://docs.djangoproject.com/en/dev/_objects/"
+ },
+ {
+ "package": "stdlib",
+ "base_url": "https://docs.python.org/3.7/",
+ "inventory_url": "https://docs.python.org/3.7/objects.inv"
+ }
+]
diff --git a/pysite/tables.py b/pysite/tables.py
index b33f04b9..7e43fe87 100644
--- a/pysite/tables.py
+++ b/pysite/tables.py
@@ -222,5 +222,15 @@ TABLES = {
"table",
"version"
])
- )
+ ),
+
+ "pydoc_links": Table( # pydoc_links
+ primary_key="package",
+ keys=sorted([
+ "base_url",
+ "inventory_url",
+ "package"
+ ]),
+ locked=False
+ ),
}
diff --git a/pysite/views/api/bot/doc.py b/pysite/views/api/bot/doc.py
new file mode 100644
index 00000000..c1d6020c
--- /dev/null
+++ b/pysite/views/api/bot/doc.py
@@ -0,0 +1,98 @@
+from flask import jsonify
+from schema import Optional, Schema
+
+from pysite.base_route import APIView
+from pysite.constants import ValidationTypes
+from pysite.decorators import api_key, api_params
+from pysite.mixins import DBMixin
+
+
+GET_SCHEMA = Schema([
+ {
+ Optional("package"): str
+ }
+])
+
+POST_SCHEMA = Schema([
+ {
+ "package": str,
+ "base_url": str,
+ "inventory_url": str
+ }
+])
+
+DELETE_SCHEMA = Schema([
+ {
+ "package": str
+ }
+])
+
+
+class DocView(APIView, DBMixin):
+ path = "/bot/docs"
+ name = "bot.docs"
+ table_name = "pydoc_links"
+
+ @api_key
+ @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params)
+ def get(self, params=None):
+ """
+ Fetches documentation metadata from the database.
+
+ - If `package` parameters are provided, fetch metadata
+ for the given packages, or `[]` if none matched.
+
+ - If `package` is not provided, return all
+ packages known to the database.
+
+ Data must be provided as params.
+ API key must be provided as header.
+ """
+
+ if params:
+ packages = (param['package'] for param in params if 'package' in param)
+ data = self.db.get_all(self.table_name, *packages, index='package') or []
+ else:
+ data = self.db.pluck(self.table_name, ("package", "base_url", "inventory_url")) or []
+
+ return jsonify(data)
+
+ @api_key
+ @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json)
+ def post(self, json_data):
+ """
+ Adds one or more new documentation metadata objects.
+
+ If the `package` passed in the data
+ already exists, it will be updated instead.
+
+ Data must be provided as JSON.
+ API key must be provided as header.
+ """
+
+ packages_to_insert = (
+ {
+ "package": json_object["package"],
+ "base_url": json_object["base_url"],
+ "inventory_url": json_object["inventory_url"]
+ } for json_object in json_data
+ )
+
+ self.db.insert(self.table_name, *packages_to_insert, conflict="update")
+ return jsonify({"success": True})
+
+ @api_key
+ @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json)
+ def delete(self, json_data):
+ """
+ Deletes a documentation metadata object.
+ Expects the `package` to be deleted to
+ be specified as a request parameter.
+
+ Data must be provided as params.
+ API key must be provided as header.
+ """
+
+ packages = (json_object["package"]for json_object in json_data)
+ changes = self.db.delete(self.table_name, *packages, return_changes=True)
+ return jsonify(changes)
diff --git a/tests/test_api.py b/tests/test_api.py
index a0b22846..b20dc0c5 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,6 +1,6 @@
from tests import SiteTest, app
-class ApiEndpointsRootEndpoints(SiteTest):
+class ApiEndpoints(SiteTest):
""" Test cases for the api subdomain """
def test_api_unknown_route(self):
""" Check api unknown route """
diff --git a/tests/test_api_docs.py b/tests/test_api_docs.py
new file mode 100644
index 00000000..945a73bc
--- /dev/null
+++ b/tests/test_api_docs.py
@@ -0,0 +1,95 @@
+import json
+
+from tests import SiteTest, app
+
+
+class ApiDocsEndpoint(SiteTest):
+ """ Check documentation metadata API """
+
+ bad_data = json.dumps({'package': 'whatever', 'correct format': False})
+ unknown_package_json = json.dumps({'package': "whatever"})
+
+ def test_api_docs_get_all(self):
+ """ GET - all entries """
+ response = self.client.get('/bot/docs', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'])
+ self.assertEqual(response.status_code, 200)
+ self.assertIsInstance(response.json, list)
+
+ def test_api_docs_get_unknown(self):
+ """ GET - unknown package """
+ response = self.client.get('/bot/docs?package=whatever', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json, [])
+
+ def test_api_docs_get_multi_unknown(self):
+ """ GET - multiple unknown packages """
+ response = self.client.get('/bot/docs?package=whatever&package=everwhat', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'])
+ self.assertEqual(response.status_code, 200)
+ self.assertIsInstance(response.json, list)
+
+ def test_api_docs_post_no_data(self):
+ """ POST - no data """
+ response = self.client.post('/bot/docs', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'])
+ self.assertEqual(response.status_code, 400)
+
+ def test_api_docs_post_bad_data(self):
+ """ POST - malformed data """
+ response = self.client.post('/bot/docs', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'], data=self.bad_data)
+ self.assertEqual(response.status_code, 400)
+
+ def test_api_docs_delete_bad(self):
+ """ DELETE - missing request body """
+ response = self.client.delete('/bot/docs', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'])
+ self.assertEqual(response.status_code, 400)
+
+ def test_api_docs_delete_unknown(self):
+ """ DELETE - unknown package """
+ response = self.client.delete('/bot/docs', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'], data=self.unknown_package_json)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json['deleted'], 0)
+
+
+class SinglePackageApiDocsEndpointTests(SiteTest):
+ """ Test the API docs endpoint with a single package added """
+
+ valid_data = {
+ 'package': "lemonapi",
+ 'base_url': "http://example.com/",
+ 'inventory_url': "http://example.com/object.inv"
+ }
+ delete_data_json = json.dumps({'package': valid_data['package']})
+ valid_data_json = json.dumps(valid_data)
+
+ def setUp(self):
+ """ POST valid data to the server for use in this test case """
+ response = self.client.post('/bot/docs', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'], data=self.valid_data_json)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json, {"success": True})
+
+ def test_api_docs_get_valid(self):
+ """ GET - added package is in all entries """
+ response = self.client.get('/bot/docs', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'])
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(self.valid_data, response.json)
+
+ def test_api_docs_get_detail(self):
+ """ GET - added package detail """
+ response = self.client.get(
+ f'/bot/docs?package={self.valid_data["package"]}', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER']
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json, [self.valid_data])
+
+ def test_api_docs_get_partly_known(self):
+ """ GET - added package is the only package for query with another unknown package """
+ response = self.client.get(
+ f'/bot/docs?package={self.valid_data["package"]}&package=whatever', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER']
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json, [self.valid_data])
+
+ def test_api_docs_delete_all(self):
+ """ DELETE - added package """
+ response = self.client.delete('/bot/docs', app.config['API_SUBDOMAIN'], headers=app.config['TEST_HEADER'], data=self.delete_data_json)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json['deleted'], 1)