1
0
mirror of https://github.com/ilri/dspace-statistics-api.git synced 2025-09-08 21:01:46 +02:00

Compare commits

..

7 Commits

Author SHA1 Message Date
a02211fd60 Update requirements
Some checks failed
continuous-integration/drone/push Build is failing
Generated with poetry export:

    $ poetry export --without-hashes -f requirements.txt > requirements.txt
    $ poetry export --without-hashes --dev -f requirements.txt > requirements-dev.txt

The `--without-hashes` is required to work around an issue with
gunicorn pulling in a dependency on setuptools that poetry ignores.

See: https://github.com/python-poetry/poetry/issues/1584
2020-12-25 13:03:32 +02:00
fc814593c7 Use my fork of falcon-swagger-ui
It has a newer Swagger UI (v3.38.0).
2020-12-25 12:57:58 +02:00
7de1084f60 Add whitespace before vim modeline
All checks were successful
continuous-integration/drone/push Build is passing
black wants this...
2020-12-24 13:12:06 +02:00
6b78e82fe9 Add vim modeline to all tests 2020-12-24 13:11:12 +02:00
4004515967 pyproject.toml: Update description
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 16:15:46 +02:00
d1229c2387 Adjust docs at root
Don't use a static HTML file anymore. Now I simply print an XHTML
page from the Falcon resource. This way I can use variables to add
in the API version as well as a link to the Swagger UI.

The list of API calls is still present on the README.md, though in
the long run I might move them to some dedicated documentation or
a GitHub wiki.
2020-12-23 16:12:50 +02:00
be83514de1 Re-work Swagger UI configuration
All checks were successful
continuous-integration/drone/push Build is passing
It turns out that Swagger UI mostly does the "right" thing for our
use cases here, but it assumes that API paths are relative to the
root of the host where it is being served. This works in the local
development environment because we are serving on "/", but it does
not work in production where the API is deployed beneath the DSpace
REST API, for example at "/rest/statistics".

The solution here is to allow configuration of the DSpace Statistics
API path and use that when registering the Swagger UI as well as in
a new "server" block in the OpenAPI JSON schema.

By default it is configured to work out of the box in a development
environment. Set the DSPACE_STATISTICS_API_URL environment variable
to something like "/rest/statistics" when running in production.
2020-12-23 13:25:17 +02:00
11 changed files with 78 additions and 68 deletions

View File

@@ -1,21 +1,34 @@
import json
import falcon
import psycopg2.extras
from falcon_swagger_ui import register_swaggerui_app
from .config import DSPACE_STATISTICS_API_URL, VERSION
from .database import DatabaseManager
from .stats import get_downloads, get_views
from .util import set_statistics_scope, validate_post_parameters
from .config import VERSION
from .config import SWAGGERUI_URL
from .config import SWAGGERUI_SCHEMA_URL
class RootResource:
def on_get(self, req, resp):
resp.status = falcon.HTTP_200
resp.content_type = "text/html"
with open("dspace_statistics_api/docs/index.html", "r") as f:
resp.body = f.read()
docs_html = (
"<!DOCTYPE html>"
"<html lang=\"en-US\">"
" <head>"
" <meta charset=\"UTF-8\">"
" <title>DSpace Statistics API</title>"
" </head>"
" <body>"
f" <h1>DSpace Statistics API {VERSION}</h1>"
f" <p>This site is running the <a href=\"https://github.com/ilri/dspace-statistics-api\" title=\"DSpace Statistics API project\">DSpace Statistics API</a>. For more information see the project's README.md or the interactive <a href=\"{DSPACE_STATISTICS_API_URL + '/swagger'}\">Swagger UI</a> built into this API.</p>"
" </body>"
"</html"
)
resp.body = docs_html
class StatusResource:
@@ -31,7 +44,20 @@ class OpenAPIJSONResource:
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()
# Load the openapi.json schema
data = json.load(f)
# Swagger assumes your API is at the root of the current host unless
# you configure a "servers" block in the schema. The problem is that
# I want this to work in both development and production, so we need
# to make this configurable.
#
# If the DSPACE_STATISTICS_API_URL is configured then we will add a
# server entry to the openapi.json schema before sending it.
if DSPACE_STATISTICS_API_URL != "":
data["servers"] = [{"url": DSPACE_STATISTICS_API_URL}]
resp.body = json.dumps(data)
class AllStatisticsResource:
@@ -199,16 +225,29 @@ api.add_route("/community/{id_:uuid}", SingleStatisticsResource())
api.add_route("/collections", AllStatisticsResource())
api.add_route("/collection/{id_:uuid}", SingleStatisticsResource())
# Swagger configuration
api.add_route(SWAGGERUI_SCHEMA_URL, OpenAPIJSONResource())
# Route to the Swagger UI OpenAPI schema
api.add_route("/docs/openapi.json", OpenAPIJSONResource())
# Path to host the Swagger UI. Keep in mind that Falcon will add a route for
# this automatically when we register Swagger and the path will be relative
# to the Falcon app like all other routes, not the absolute root.
SWAGGERUI_PATH = "/swagger"
# The *absolute* path to the OpenJSON schema. This must be absolute because
# it will be requested by the client and must resolve absolutely. Note: the
# name of this variable is misleading because it is actually the schema URL
# but we pass it into the register_swaggerui_app() function as the api_url
# parameter.
SWAGGERUI_API_URL = f"{DSPACE_STATISTICS_API_URL}/docs/openapi.json"
register_swaggerui_app(
api,
SWAGGERUI_URL,
SWAGGERUI_SCHEMA_URL,
SWAGGERUI_PATH,
SWAGGERUI_API_URL,
config={
"supportedSubmitMethods": ["get", "post"],
},
uri_prefix=DSPACE_STATISTICS_API_URL,
)
# vim: set sw=4 ts=4 expandtab:

View File

@@ -9,12 +9,12 @@ DATABASE_PASS = os.environ.get("DATABASE_PASS", "dspacestatistics")
DATABASE_HOST = os.environ.get("DATABASE_HOST", "localhost")
DATABASE_PORT = os.environ.get("DATABASE_PORT", "5432")
# SwaggerUI configuration
# URI path where the Swagger UI should be available (without trailing slash)
SWAGGERUI_URL = os.environ.get("SWAGGERUI_URL", "/swagger")
# URI path to the OpenAPI JSON schema
SWAGGERUI_SCHEMA_URL = os.environ.get("SWAGGERUI_SCHEMA_URL", "/docs/openapi.json")
# URL to DSpace Statistics API, which will be used as a prefix to API calls in
# the Swagger UI. An empty string will allow this to work out of the box in a
# local development environment, but for production it should be set to a value
# like "/rest/statistics", assuming that the statistics API is deployed next to
# the vanilla DSpace REST API.
DSPACE_STATISTICS_API_URL = os.environ.get("DSPACE_STATISTICS_API_URL", "")
VERSION = "1.4.0-dev"

View File

@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>DSpace Statistics API</title>
</head>
<body>
<h1>DSpace Statistics API v1.4.0-dev</h1>
<p>This site is running the <a href="https://github.com/ilri/dspace-statistics-api" title="DSpace Statistics API project">DSpace Statistics API</a>. The following endpoints are available:</p>
<ul>
<li>GET <code>/</code>return a basic API documentation page.</li>
<li>GET <code>/items</code>return views and downloads for all items that Solr knows about¹. Accepts <code>limit</code> and <code>page</code> query parameters for pagination of results (<code>limit</code> must be an integer between 1 and 100, and <code>page</code> must be an integer greater than or equal to 0).</li>
<li>POST <code>/items</code>return views and downloads for an arbitrary list of items with an optional date range. Accepts <code>limit</code>, <code>page</code>, <code>dateFrom</code>, and <code>dateTo</code> parameters².</li>
<li>GET <code>/item/id</code>return views and downloads for a single item (<code>id</code> must be a UUID). Returns HTTP 404 if an item id is not found.</li>
<li>GET <code>/communities</code>return views and downloads for all communities that Solr knows about¹. Accepts <code>limit</code> and <code>page</code> query parameters for pagination of results (<code>limit</code> must be an integer between 1 and 100, and <code>page</code> must be an integer greater than or equal to 0).
<li>POST <code>/communities</code>return views and downloads for an arbitrary list of communities with an optional date range. Accepts <code>limit</code>, <code>page</code>, <code>dateFrom</code>, and <code>dateTo</code> parameters².
<li>GET <code>/community/id</code>return views and downloads for a single community (<code>id</code> must be a UUID). Returns HTTP 404 if a community id is not found.
<li>GET <code>/collections</code>return views and downloads for all collections that Solr knows about¹. Accepts <code>limit</code> and <code>page</code> query parameters for pagination of results (<code>limit</code> must be an integer between 1 and 100, and <code>page</code> must be an integer greater than or equal to 0).
<li>POST <code>/collections</code>return views and downloads for an arbitrary list of collections with an optional date range. Accepts <code>limit</code>, <code>page</code>, <code>dateFrom</code>, and <code>dateTo</code> parameters².
<li>GET <code>/collection/id</code>return views and downloads for a single collection (<code>id</code> must be a UUID). Returns HTTP 404 if an collection id is not found.
</ul>
<p>The id is the <em>internal</em> UUID for an item, community, or collection. You can get these from the standard DSpace REST API.</p>
<hr/>
<p>¹ We are querying the Solr statistics core, which technically only knows about items, communities, or collections that have either views or downloads. If an item, community, or collection is not present here you can assume it has zero views and zero downloads, but not necessarily that it does not exist in the repository.</p>
<p>² POST requests to <code>/items</code>, <code>/communities</code>, and <code>/collections</code> should be in JSON format with the following parameters (substitute the "items" list for communities or collections accordingly):</p>
<pre><code>{
"limit": 100, // optional, integer between 1 and 100, default 100
"page": 0, // optional, integer greater than 0, default 0
"dateFrom": "2020-01-01T00:00:00Z", // optional, default *
"dateTo": "2020-09-09T00:00:00Z", // optional, default *
"items": [
"f44cf173-2344-4eb2-8f00-ee55df32c76f",
"2324aa41-e9de-4a2b-bc36-16241464683e",
"8542f9da-9ce1-4614-abf4-f2e3fdb4b305",
"0fe573e7-042a-4240-a4d9-753b61233908"
]
}</code></pre>
</p>
</body>
</html>

11
poetry.lock generated
View File

@@ -144,6 +144,11 @@ python-versions = "*"
falcon = "*"
Jinja2 = "*"
[package.source]
url = "https://github.com/alanorth/falcon-swagger-ui.git"
reference = "a44244c85dceccfcd249b62fea4ee82a8221e3d2"
type = "git"
[[package]]
name = "flake8"
version = "3.8.4"
@@ -589,7 +594,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 = "1d56758c9e3aa4586109e8aaf4def576d3506385bc749b97e8518f936f2e91ca"
content-hash = "3a8e8a7152971ae091864e7dd5a8fd25f895f68dd1444bffc153c61227e84914"
[metadata.files]
appdirs = [
@@ -655,9 +660,7 @@ 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"},
]
falcon-swagger-ui = []
flake8 = [
{file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
{file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"},

View File

@@ -1,7 +1,7 @@
[tool.poetry]
name = "dspace-statistics-api"
version = "1.4.0-dev"
description = "A simple REST API to expose Solr view and download statistics for items in a DSpace repository."
description = "A simple REST API to expose Solr view and download statistics for items, communities, and collections in a DSpace repository."
authors = ["Alan Orth <aorth@mjanja.ch>"]
license = "GPL-3.0-only"
@@ -11,7 +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"
falcon-swagger-ui = {git = "https://github.com/alanorth/falcon-swagger-ui.git"}
[tool.poetry.dev-dependencies]
ipython = { version = "^7.18.1", python = "^3.7" }

View File

@@ -11,7 +11,7 @@ colorama==0.4.4; python_version >= "3.7" and python_version < "4.0" and sys_plat
dataclasses==0.6; python_version < "3.7"
decorator==4.4.2; python_version >= "3.7" and python_version < "4.0"
falcon==2.0.0
falcon-swagger-ui==1.2.1
-e git+https://github.com/alanorth/falcon-swagger-ui.git@a44244c85dceccfcd249b62fea4ee82a8221e3d2#egg=falcon-swagger-ui
flake8==3.8.4
gunicorn==20.0.4
idna==2.10

View File

@@ -1,7 +1,7 @@
certifi==2020.12.5
chardet==4.0.0
falcon==2.0.0
falcon-swagger-ui==1.2.1
-e git+https://github.com/alanorth/falcon-swagger-ui.git@a44244c85dceccfcd249b62fea4ee82a8221e3d2#egg=falcon-swagger-ui
gunicorn==20.0.4
idna==2.10
jinja2==2.11.2

View File

@@ -371,3 +371,6 @@ def test_post_collections_invalid_page(client):
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 400
# vim: set sw=4 ts=4 expandtab:

View File

@@ -371,3 +371,6 @@ def test_post_communities_invalid_page(client):
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 400
# vim: set sw=4 ts=4 expandtab:

View File

@@ -43,3 +43,6 @@ def test_get_status(client):
assert isinstance(response.content, bytes)
assert response.status_code == 200
# vim: set sw=4 ts=4 expandtab:

View File

@@ -371,3 +371,6 @@ def test_post_items_invalid_page(client):
response = client.simulate_post("/items", json=request_body)
assert response.status_code == 400
# vim: set sw=4 ts=4 expandtab: