diff options
author | 2018-06-05 15:14:52 +0200 | |
---|---|---|
committer | 2018-06-05 14:14:52 +0100 | |
commit | b8c2be0b446be35c273ccaf5ff10fd980be0edcc (patch) | |
tree | c1a068d3e43420e26626c2fd53e1e8c06da6c24c | |
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__.py | 0 | ||||
-rw-r--r-- | pysite/migrations/tables/pydoc_links/initial_data.json | 22 | ||||
-rw-r--r-- | pysite/tables.py | 12 | ||||
-rw-r--r-- | pysite/views/api/bot/doc.py | 98 | ||||
-rw-r--r-- | tests/test_api.py | 2 | ||||
-rw-r--r-- | tests/test_api_docs.py | 95 |
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) |