diff options
| -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) | 
