Add Swagger UI on /swagger

This includes a Swagger UI with an OpenAPI 3.0 JSON schema for easy
interactive demonstration and testing of the API. The JSON schema
was created with the standalone swagger-editor. Includes tests to
make sure that the /swagger and /docs/openapi.json paths are acce-
ssible.
This commit is contained in:
Alan Orth 2020-12-22 11:18:47 +02:00
parent 3e271c7852
commit a35ecf2394
Signed by: alanorth
GPG Key ID: 0FB860CC9C45B1B9
5 changed files with 705 additions and 1 deletions

View File

@ -1,5 +1,6 @@
import falcon
import psycopg2.extras
from falcon_swagger_ui import register_swaggerui_app
from .database import DatabaseManager
from .stats import get_downloads, get_views
@ -14,6 +15,14 @@ class RootResource:
resp.body = f.read()
class OpenAPIJSONResource:
def on_get(self, req, resp):
resp.status = falcon.HTTP_200
resp.content_type = "text/html"
with open("dspace_statistics_api/docs/openapi.json", "r") as f:
resp.body = f.read()
class AllStatisticsResource:
@falcon.before(set_statistics_scope)
def on_get(self, req, resp):
@ -178,4 +187,18 @@ api.add_route("/community/{id_:uuid}", SingleStatisticsResource())
api.add_route("/collections", AllStatisticsResource())
api.add_route("/collection/{id_:uuid}", SingleStatisticsResource())
# Swagger configuration
SWAGGERUI_URL = "/swagger" # without trailing slash
SCHEMA_URL = "/docs/openapi.json"
api.add_route("/docs/openapi.json", OpenAPIJSONResource())
register_swaggerui_app(
api,
SWAGGERUI_URL,
SCHEMA_URL,
config={
"supportedSubmitMethods": ["get", "post"],
},
)
# vim: set sw=4 ts=4 expandtab:

View File

@ -0,0 +1,586 @@
{
"openapi": "3.0.3",
"info": {
"version": "1.4.0-dev",
"title": "DSpace Statistics API",
"description": "A [Falcon-based](https://falcon.readthedocs.io/) web application to make DSpace's item, community, and collection statistics available via a simple REST API. This Swagger interface is powered by [falcon-swagger-ui](https://github.com/rdidyk/falcon-swagger-ui).",
"license": {
"name": "GPLv3.0",
"url": "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
},
"paths": {
"/item/{item_uuid}": {
"get": {
"summary": "Statistics for a specific item",
"operationId": "getItem",
"tags": [
"item"
],
"parameters": [
{
"name": "item_uuid",
"in": "path",
"required": true,
"description": "The UUID of the item to retrieve",
"schema": {
"type": "string",
"format": "uuid",
"example": "9596aeff-0b90-47d3-9fec-02d578920507"
}
}
],
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SingleElementResponse"
}
}
}
},
"404": {
"description": "Item not found"
}
}
}
},
"/items": {
"get": {
"summary": "Get statistics for all items",
"operationId": "getItems",
"tags": [
"items"
],
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at once (optional)",
"required": false,
"schema": {
"type": "integer",
"format": "int32",
"minimum": 1,
"maximum": 100,
"default": 100,
"example": 100
}
},
{
"name": "page",
"in": "query",
"description": "Page of results to start on (optional)",
"required": false,
"schema": {
"type": "integer",
"format": "int32",
"minimum": 0,
"default": 0,
"example": 0
}
}
],
"responses": {
"200": {
"description": "A paged array of items",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SingleElementResponse"
}
}
}
},
"400": {
"description": "Bad request"
}
}
},
"post": {
"summary": "Get statistics for a list of items with an optional date range",
"operationId": "postItems",
"tags": [
"items"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"format": "int32",
"minimum": 1,
"maximum": 100,
"default": 100
},
"page": {
"type": "integer",
"format": "int32",
"minimum": 0,
"default": 0
},
"dateFrom": {
"type": "string",
"format": "date"
},
"dateTo": {
"type": "string",
"format": "date"
},
"items": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
}
},
"example": {
"limit": 100,
"page": 5,
"dateFrom": "2020-01-01T00:00:00Z",
"dateTo": "2020-12-31T00:00:00Z",
"items": [
"f44cf173-2344-4eb2-8f00-ee55df32c76f",
"2324aa41-e9de-4a2b-bc36-16241464683e",
"8542f9da-9ce1-4614-abf4-f2e3fdb4b305",
"0fe573e7-042a-4240-a4d9-753b61233908"
]
}
}
}
}
},
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"currentPage": {
"type": "integer",
"format": "int32"
},
"limit": {
"type": "integer",
"format": "int32"
},
"totalPages": {
"type": "integer",
"format": "int32"
},
"statistics": {
"$ref": "#/components/schemas/ListOfElements"
}
}
}
}
}
},
"400": {
"description": "Bad request"
}
}
}
},
"/community/{community_uuid}": {
"get": {
"summary": "Statistics for a specific community",
"operationId": "getCommunity",
"tags": [
"community"
],
"parameters": [
{
"name": "community_uuid",
"in": "path",
"required": true,
"description": "The UUID of the community to retrieve",
"schema": {
"type": "string",
"format": "uuid",
"example": "bde7139c-d321-46bb-aef6-ae70799e5edb"
}
}
],
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SingleElementResponse"
}
}
}
},
"404": {
"description": "Community not found"
}
}
}
},
"/communities": {
"get": {
"summary": "Get statistics for all communities",
"operationId": "getCommunities",
"tags": [
"communities"
],
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many communities to return at once (optional)",
"required": false,
"schema": {
"type": "integer",
"format": "int32",
"minimum": 1,
"maximum": 100,
"default": 100,
"example": 100
}
},
{
"name": "page",
"in": "query",
"description": "Zero-based page of results to start on (optional)",
"required": false,
"schema": {
"type": "integer",
"format": "int32",
"minimum": 0,
"default": 0,
"example": 0
}
}
],
"responses": {
"200": {
"description": "A paged array of communities",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SingleElementResponse"
}
}
}
},
"400": {
"description": "Bad request"
}
}
},
"post": {
"summary": "Get statistics for a list of communities with an optional date range",
"operationId": "postCommunities",
"tags": [
"communities"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"format": "int32",
"minimum": 1,
"maximum": 100,
"default": 100
},
"page": {
"type": "integer",
"format": "int32",
"minimum": 0,
"default": 0
},
"dateFrom": {
"type": "string",
"format": "date"
},
"dateTo": {
"type": "string",
"format": "date"
},
"communities": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
}
},
"example": {
"limit": 100,
"page": 0,
"dateFrom": "2020-01-01T00:00:00Z",
"dateTo": "2020-12-31T00:00:00Z",
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"8a8aeed1-077e-4360-bdf8-a5f3020193b1",
"47d0498a-203c-407d-afb8-1d44bf29badc",
"d3fe99a9-e27d-4035-9339-084c93228c82"
]
}
}
}
}
},
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"currentPage": {
"type": "integer",
"format": "int32"
},
"limit": {
"type": "integer",
"format": "int32"
},
"totalPages": {
"type": "integer",
"format": "int32"
},
"statistics": {
"$ref": "#/components/schemas/ListOfElements"
}
}
}
}
}
},
"400": {
"description": "Bad request"
}
}
}
},
"/collection/{collection_uuid}": {
"get": {
"summary": "Statistics for a specific collection",
"operationId": "getCollection",
"tags": [
"collection"
],
"parameters": [
{
"name": "collection_uuid",
"in": "path",
"required": true,
"description": "The UUID of the collection to retrieve",
"schema": {
"type": "string",
"format": "uuid",
"example": "49dc95d8-bf2f-4e68-b30f-41ea266c37ae"
}
}
],
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SingleElementResponse"
}
}
}
},
"404": {
"description": "Collection not found"
}
}
}
},
"/collections": {
"get": {
"summary": "Get statistics for all collections",
"operationId": "getCollections",
"tags": [
"collections"
],
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many collections to return at once (optional)",
"required": false,
"schema": {
"type": "integer",
"format": "int32",
"minimum": 1,
"maximum": 100,
"default": 100,
"example": 100
}
},
{
"name": "page",
"in": "query",
"description": "Zero-based page of results to start on (optional)",
"required": false,
"schema": {
"type": "integer",
"format": "int32",
"minimum": 0,
"default": 0,
"example": 0
}
}
],
"responses": {
"200": {
"description": "A paged array of collections",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SingleElementResponse"
}
}
}
},
"400": {
"description": "Bad request"
}
}
},
"post": {
"summary": "Get statistics for a list of collections with an optional date range",
"operationId": "postCollections",
"tags": [
"collections"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"format": "int32",
"minimum": 1,
"maximum": 100,
"default": 100
},
"page": {
"type": "integer",
"format": "int32",
"minimum": 0,
"default": 0
},
"dateFrom": {
"type": "string",
"format": "date"
},
"dateTo": {
"type": "string",
"format": "date"
},
"collections": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
}
},
"example": {
"limit": 100,
"page": 2,
"dateFrom": "2020-01-01T00:00:00Z",
"dateTo": "2020-12-31T00:00:00Z",
"collections": [
"5eeef6cf-b91b-42d0-9549-ea61bc8a758f",
"6aac3269-b4a9-4924-a24d-9e6ee2b410d2",
"551698dd-cd2b-4327-948e-54b5eb6deda5",
"39358713-bbaf-4149-a453-e2b18c09fd5d"
]
}
}
}
}
},
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"currentPage": {
"type": "integer",
"format": "int32"
},
"limit": {
"type": "integer",
"format": "int32"
},
"totalPages": {
"type": "integer",
"format": "int32"
},
"statistics": {
"$ref": "#/components/schemas/ListOfElements"
}
}
}
}
}
},
"400": {
"description": "Bad request"
}
}
}
}
},
"components": {
"schemas": {
"SingleElementResponse": {
"type": "object",
"required": [
"id",
"views",
"downloads"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"views": {
"type": "integer",
"example": 450
},
"downloads": {
"type": "integer",
"example": 1337
}
}
},
"ListOfElements": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SingleElementResponse"
}
}
}
}
}

78
poetry.lock generated
View File

@ -132,6 +132,18 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "falcon-swagger-ui"
version = "1.2.1"
description = "Swagger UI Application for Falcon"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
falcon = "*"
Jinja2 = "*"
[[package]]
name = "flake8"
version = "3.8.4"
@ -273,6 +285,28 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"]
[package.dependencies]
parso = ">=0.7.0,<0.8.0"
[[package]]
name = "jinja2"
version = "2.11.2"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
i18n = ["Babel (>=0.8)"]
[package.dependencies]
MarkupSafe = ">=0.23"
[[package]]
name = "markupsafe"
version = "1.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[[package]]
name = "mccabe"
version = "0.6.1"
@ -555,7 +589,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt
[metadata]
lock-version = "1.0"
python-versions = "^3.6"
content-hash = "3cd45aacbfab0e85f74c7a010443432f4d6bf0f6cd3bbb84e11c8c7ea20a4613"
content-hash = "1d56758c9e3aa4586109e8aaf4def576d3506385bc749b97e8518f936f2e91ca"
[metadata.files]
appdirs = [
@ -621,6 +655,9 @@ falcon = [
{file = "falcon-2.0.0-py2.py3-none-any.whl", hash = "sha256:54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53"},
{file = "falcon-2.0.0.tar.gz", hash = "sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc"},
]
falcon-swagger-ui = [
{file = "falcon_swagger_ui-1.2.1-py3-none-any.whl", hash = "sha256:2514e6cb403e87e49a1527764cf090c82885185cc650b7ab5cefa8ebe89af8b8"},
]
flake8 = [
{file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
{file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"},
@ -657,6 +694,45 @@ jedi = [
{file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"},
{file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"},
]
jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},

View File

@ -11,6 +11,7 @@ gunicorn = "^20.0.4"
falcon = "^2.0.0"
psycopg2-binary = "^2.8.6"
requests = "^2.24.0"
falcon-swagger-ui = "^1.2.1"
[tool.poetry.dev-dependencies]
ipython = { version = "^7.18.1", python = "^3.7" }

View File

@ -16,3 +16,21 @@ def test_get_docs(client):
assert isinstance(response.content, bytes)
assert response.status_code == 200
def test_get_openapi_json(client):
"""Test requesting the OpenAPI JSON schema."""
response = client.simulate_get("/docs/openapi.json")
assert isinstance(response.content, bytes)
assert response.status_code == 200
def test_get_swagger_ui(client):
"""Test requesting the Swagger UI."""
response = client.simulate_get("/swagger")
assert isinstance(response.content, bytes)
assert response.status_code == 200