1
0
mirror of https://github.com/ilri/dspace-statistics-api.git synced 2025-07-04 21:43:26 +02:00

Compare commits

...

94 Commits

Author SHA1 Message Date
1a1a14a25f Version 1.4.3 2022-03-26 19:08:56 +03:00
c09fc789e8 Update requirements
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
2022-03-26 19:06:54 +03:00
134a4f1595 poetry.lock: run poetry update 2022-03-26 18:50:31 +03:00
12ebd1aed5 pyproject.toml: falcon 3.1.0
Doesn't seem to have any breaking changes, only fixes and some new
compatability updates with new Pythons.

See: https://falcon.readthedocs.io/en/stable/changes/3.1.0.html
2022-03-26 18:49:33 +03:00
e5f3201b65 Update requirements
All checks were successful
continuous-integration/drone/push Build is passing
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
2022-03-21 15:19:00 +03:00
c1ce4fe233 poetry.lock: run poetry update 2022-03-21 15:18:27 +03:00
b2eb1878a5 .github/workflows/python-app.yml: quote python version
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-30 18:57:10 +03:00
a0213c1c97 poetry.lock: run poetry update
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-30 13:52:20 +03:00
cd03ca2b36 Update requirements
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
2022-01-30 13:50:49 +03:00
c48e6a79c7 pyproject.toml: update dependencies
We no longer need ipython because it's installed globally on all
my machines. Also, new major version of flake8 and black is no
longer a beta.
2022-01-30 13:49:36 +03:00
a2e1695ecc Update requirements
All checks were successful
continuous-integration/drone/push Build is passing
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
2021-12-19 14:18:42 +02:00
b683bf211c .github/workflows/python-app.yml: use Python 3.10 2021-12-19 14:15:41 +02:00
3ab48743d6 poetry.lock: run poetry update 2021-12-19 14:13:14 +02:00
88173eaae9 README.md: fix link to actions
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-08 11:34:50 +02:00
f557d33f36 README.md: adjust intro
Use intro style from Python Black! This makes it easier to have the
badges displayed without wrapping and looks nicer.
2021-12-08 09:48:31 +02:00
ffc4ff4a5c 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
2021-11-11 09:05:37 +02:00
7551b34632 poetry.lock: run poetry update
This was previously failing for the past few days.
2021-11-11 09:05:03 +02:00
5e71ec10eb Remove pipenv
Poetry's working again.
2021-11-11 09:04:30 +02:00
f80d360cf9 Only install ipython on Python 3.7+
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-10 09:21:59 +02:00
e70b59ecfe Update requirements
Some checks failed
continuous-integration/drone/push Build is failing
Generated with pipenv lock:

    $ pipenv lock -r > requirements.txt
    $ pipenv lock -r --dev > requirements-dev.txt
2021-11-09 22:52:21 +02:00
4d0828b6c0 Add Pipenv configuration
I was having a problem with Poetry.
2021-11-09 22:51:23 +02:00
dabc4c0259 pyproject.toml: revert to my fork of falcon-swagger-ui
The Falcon 3 fix never actually got committed to rdidyik's fork. I
have submitted a new pull request and will use my fork until it is
merged.

See: https://github.com/rdidyk/falcon-swagger-ui/pull/21
2021-11-09 22:09:11 +02:00
4fd8af07c3 .drone.yml: Fix job name 2021-11-09 17:36:48 +02:00
4c5326a176 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
2021-10-21 15:07:26 +03:00
3b1ccafab4 poetry.lock: Run poetry update 2021-10-21 15:06:19 +03:00
58b5ae82d3 pyproject.toml: Switch back to falcon-swagger-ui upstream
They merged my changes for Falcon 3.0.

See: https://github.com/rdidyk/falcon-swagger-ui/pull/20
2021-10-21 15:04:58 +03:00
562aaeef7d .drone.yml: Test on Python 3.10
Some checks failed
continuous-integration/drone/push Build is failing
2021-10-11 20:11:32 +03:00
5cdba6acb1 .drone.yml: Also install gcc for all Python containers
All checks were successful
continuous-integration/drone/push Build is passing
We previously only needed gcc for typed-ast in Python 3.9, but now
we actually need gcc to compile psycopg2 in all of them.
2021-07-06 16:44:13 +03:00
dd0937179c .drone.yml: Add libpq-dev to test container
Some checks reported errors
continuous-integration/drone/push Build was killed
We need it to compile the psycopg2 Python library.
2021-07-06 16:41:17 +03:00
f0c6c004db 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
2021-07-06 16:27:23 +03:00
6843f0a8ac poetry.lock: Run poetry update 2021-07-06 16:26:33 +03:00
f5fcfcc05a pyproject.toml: Update psycopg2 version
I manually re-installed psycopg2@latest while troubleshooting an
issue with it not working after Arch Linux updated Python. That's
one down side of using the non-binary package.
2021-07-06 16:26:05 +03:00
e8ac74b6d1 pyproject.toml: Update some dev dependencies 2021-07-06 16:17:22 +03:00
14fc14daee 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
2021-06-22 20:53:07 +03:00
871aae537a poetry.lock: Sync changes 2021-06-22 20:52:15 +03:00
2fada6c6ff pyproject.toml: Use psycopg2 instead of psycopg2-binary
All checks were successful
continuous-integration/drone/push Build is passing
According to the documentation the binary version is not meant to
be run in production. Since I'm in control of both my development
and production servers and can ensure that libpq-dev is installed
on both, I will use the source version of this module.

See: https://www.psycopg.org/docs/install.html#quick-install
2021-06-22 17:49:49 +03:00
ef0991e352 Update requirements
All checks were successful
continuous-integration/drone/push Build is passing
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
2021-06-22 10:11:57 +03:00
4502d6053c poetry.lock: run poetry update
The following packages were updated:

> markupsafe (2.0.0 -> 2.0.1)
> certifi (2020.12.5 -> 2021.5.30)
> click (8.0.0 -> 8.0.1)
> decorator (5.0.7 -> 5.0.9)
> jinja2 (3.0.0 -> 3.0.1)
> prompt-toolkit (3.0.18 -> 3.0.19)
> urllib3 (1.26.4 -> 1.26.5)
> ipython (7.23.1 -> 7.24.1)
> psycopg2-binary (2.8.6 -> 2.9.1)
2021-06-22 10:10:29 +03:00
a524068cf6 Bump version to 1.4.3-dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-15 14:44:44 +03:00
964d5dff06 Version 1.4.2 2021-04-15 14:23:07 +03:00
a9252d1771 Update requirements-dev.txt
Generated with poetry export:

    $ 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
2021-04-15 14:19:48 +03:00
a63687d516 poetry.lock: Run poetry update 2021-04-15 14:17:17 +03:00
73dc3a292e README.md: Remove TODO about Swagger
All checks were successful
continuous-integration/drone/push Build is passing
I added the SwaggerUI interface a few months ago.
2021-04-06 20:28:10 +03:00
1e742bad41 CHANGELOG.md: Add note about valid page tests
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-06 09:07:51 +03:00
164008981e CHANGELOG.md: Add notes about Falcon 3.0.0
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-06 08:58:00 +03:00
dd1769b954 tests: Fix totalPages
A few months ago I fixed the totalPages display to show 1 when we
only have one page of results (the page itself is still 0), but I
didn't update the tests.

See: 4f8cd1097b
2021-04-06 08:54:54 +03:00
b009820fb4 Update requirements
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
2021-04-06 08:32:22 +03:00
9830295978 poetry.lock: Run poetry update 2021-04-06 08:31:50 +03:00
c93a4d7455 pyproject.toml: Falcon 3.0.0
Release notes: https://falcon.readthedocs.io/en/latest/changes/3.0.0.html
2021-04-06 08:31:39 +03:00
2f8e4f8a0a Changes for Falcon 3.0.0
Mostly it seems we just need to use resp.text instead of resp.body,
including in falcon-swagger-ui (I forked the upstream one to make
this change).

See: https://falcon.readthedocs.io/en/latest/changes/3.0.0.html
2021-04-06 08:30:28 +03:00
0650c5985e Add SPDX short license identifier to all Python files
All checks were successful
continuous-integration/drone/push Build is passing
See: https://spdx.github.io/spdx-spec/appendix-V-using-SPDX-short-identifiers-in-source-files/
2021-03-22 13:42:42 +02:00
d814f1c4f0 CHANGELOG.md: Fix heading
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-21 19:50:39 +02:00
00f30591c4 CHANGELOG.md: Add notes about GitHub Actions 2021-03-21 19:49:35 +02:00
acfe87b91a Add GitHub Actions badge and remove sr.ht
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-21 11:48:05 +02:00
bc6d84dda2 Add GitHub Actions workflow
My first time setting up a PostgreSQL service container on GitHub
actions. Note that there are two different kinds of environment
variables: those passed to the Docker container, and those used by
the PostgreSQL utilities.

See: https://docs.github.com/en/actions/guides/creating-postgresql-service-containers
See: https://hub.docker.com/_/postgres
2021-03-21 11:44:39 +02:00
889fb2f74a Update requirements
All checks were successful
continuous-integration/drone/push Build is passing
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
2021-03-21 08:59:41 +02:00
c42cd7a818 poetry.lock: Run poetry update 2021-03-21 08:59:04 +02:00
f8bba59d66 .gitignore: Ignore .egg-info
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-14 21:50:47 +02:00
b8cb752a29 CHANGELOG.md: Add note about updated poetry deps
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-11 11:23:18 +02:00
09496aa2b5 Update requirements
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
2021-03-11 11:22:05 +02:00
ff5dc7506d poetry.lock: Run poetry update 2021-03-11 11:21:02 +02:00
80a11ead97 Version 1.4.1
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-14 14:19:50 +02:00
a282c95933 CHANGELOG.md: Minor syntax fix 2021-01-14 14:15:57 +02:00
fd7cc36306 Update requirements-dev.txt
Generated with poetry export:

    $ 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
2021-01-14 14:14:16 +02:00
a20ff09570 poetry.lock: Run poetry update
All tests still pass.
2021-01-14 14:13:32 +02:00
fdc0e73088 tests: Sort imports with isort 2021-01-14 14:12:59 +02:00
b15afc9f39 CHANGELOG.md: Add note about UUIDs
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-05 12:41:21 +02:00
2bc18ef719 README.md: Make a note about migrating UUIDs 2021-01-05 12:35:23 +02:00
49751b53f0 dspace_statistics_api/indexer.py: Limit to UUIDs
We need to make sure that the indexer only tries to index UUIDs, as
opposed to legacy IDs that may have been left over from a migration
from earlier DSpace versions. For example, "98110-unmigrated", "-1"
etc.

For matching the UUIDs in Solr I decided that it is sufficient for
our use case to simply match thirty-six characters, where a UUID is
composed of thirty-two hexadecimal characters and four dashes. We
don't need to do any verification of "real" UUIDs because it would
be needlessly complex in our case.

See: https://github.com/ilri/dspace-statistics-api/issues/12
2021-01-05 12:30:27 +02:00
d1c177e146 .drone.yml: Add git to python container
All checks were successful
continuous-integration/drone/push Build is passing
Now that I am installing my own fork of falcon-swagger-ui we need
to have git so we can install it with pip.
2020-12-27 14:22:23 +02:00
33dc210452 dspace_statistics_api/docs/openapi.json: Minor edit
Better to leave the version in there because Swagger Editor doesn't
like it without. Also, change the example page parameter for POSTing
to /items and /collections, as it doesn't make sense to start on a
later page if we have less items than our limit.
2020-12-27 13:53:59 +02:00
282d5f644a Move unreleased change to v1.4.0 2020-12-27 12:52:24 +02:00
05e0e8bdca openapi.json: Set the API version from config
We don't need to hard code this in the JSON anymore since we are
reading and modifying it now for the server config anyways.
2020-12-27 12:48:13 +02:00
2567bb8604 dspace_statistics_api/app.py: Format with black 2020-12-27 12:27:01 +02:00
4af3c656a3 CHANGELOG.md: Add note about totalPages 2020-12-27 12:26:32 +02:00
4f8cd1097b Rework paging
The "totalPages" value in our response is calculated incorrectly.
Instead of casting to int and rounding, we should rather round up
to the next integer with math.ceil. This is a more correct way to
get the value.

Also update the indexer to use the same logic, although there the
values are printed with +1 so they are more readable.
2020-12-27 12:22:07 +02:00
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
70b2ba83ba Allow configuration of Swagger and OpenAPI JSON URL
All checks were successful
continuous-integration/drone/push Build is passing
When running in production your statistics API might be deployed to
a path like /rest/statistics instead of at the root.
2020-12-22 12:50:03 +02:00
893039bc6a Update requirements
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-22 12:07:07 +02:00
a4628dde4e Update requirements
Generated with poetry export:

    $ poetry export -f requirements.txt > requirements.txt
    $ poetry export --dev -f requirements.txt > requirements-dev.txt
2020-12-22 11:45:39 +02:00
68418ea053 dspace_statistics_api/docs/openapi.json: Add /status
Add a /status to the Swagger UI schema.
2020-12-22 11:41:47 +02:00
6bbee7919e Bump version to 1.4.0-dev 2020-12-22 11:31:46 +02:00
8f0061ce29 CHANGELOG.md: Add note about the /status page 2020-12-22 11:30:50 +02:00
4b1398c67f Add /status route
Currently this only prints the API version.
2020-12-22 11:30:09 +02:00
a9d2a6d9be CHANGELOG.md: Add note about Swagger UI 2020-12-22 11:21:46 +02:00
a35ecf2394 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.
2020-12-22 11:18:47 +02:00
3e271c7852 tests/dspacestatistics.sql: Update data
All checks were successful
continuous-integration/drone/push Build is passing
Add a new database snapshot with communities and collections.
2020-12-20 22:31:41 +02:00
d7ba14c590 tests: Add tests for communities and collections
Also, separate tests for items, communities, and collections into
their own files, leaving a single test for docs in its own file.
2020-12-20 22:12:13 +02:00
23 changed files with 3199 additions and 1661 deletions

View File

@ -1,21 +0,0 @@
image: archlinux
packages:
- python-poetry
- postgresql
sources:
- https://git.sr.ht/~alanorth/dspace-statistics-api
tasks:
- setup: |
id
psql --version
sudo su - postgres -c "initdb --locale en_US.UTF-8 -E UTF8 -D '/var/lib/postgres/data'"
sudo systemctl start postgresql
createuser -U postgres dspacestatistics
psql -U postgres -c "ALTER USER dspacestatistics WITH PASSWORD 'dspacestatistics'"
createdb -U postgres -O dspacestatistics --encoding=UNICODE dspacestatistics
cd dspace-statistics-api
psql -U postgres -d dspacestatistics < tests/dspacestatistics.sql
poetry install --no-root
- test: |
cd dspace-statistics-api
poetry run pytest

View File

@ -1,5 +1,44 @@
kind: pipeline
type: docker
name: python310
steps:
- name: setup
image: postgres:10-alpine
environment:
PGPASSWORD: postgres
commands:
- id
- psql --version
- sleep 5
- pg_isready -h database -U postgres -d dspacestatistics
- createuser -h database -U postgres dspacestatistics
- psql -h database -U postgres -c "ALTER USER dspacestatistics WITH PASSWORD 'dspacestatistics'"
- psql -h database -U postgres -d dspacestatistics < tests/dspacestatistics.sql
- name: test
image: python:3.10-slim
environment:
PGPASSWORD: dspacestatistics
DATABASE_HOST: database
commands:
- id
- python -V
- apt update && apt install -y gcc git libpq-dev
- pip install -r requirements-dev.txt
- pytest
services:
- name: database
image: postgres:10-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dspacestatistics
---
kind: pipeline
type: docker
name: python39
steps:
@ -24,7 +63,7 @@ steps:
commands:
- id
- python -V
- apt update && apt install -y gcc
- apt update && apt install -y gcc git libpq-dev
- pip install -r requirements-dev.txt
- pytest
@ -71,6 +110,7 @@ steps:
commands:
- id
- python -V
- apt update && apt install -y gcc git libpq-dev
- pip install -r requirements-dev.txt
- pytest
@ -109,6 +149,7 @@ steps:
commands:
- id
- python -V
- apt update && apt install -y gcc git libpq-dev
- pip install -r requirements-dev.txt
- pytest
@ -147,6 +188,7 @@ steps:
commands:
- id
- python -V
- apt update && apt install -y gcc git libpq-dev
- pip install -r requirements-dev.txt
- pytest

61
.github/workflows/python-app.yml vendored Normal file
View File

@ -0,0 +1,61 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Build and Test
on: ['push', 'pull_request']
jobs:
build:
runs-on: ubuntu-latest
services:
database:
image: postgres:10-alpine
env:
# password for postgres user in the Docker container
POSTGRES_PASSWORD: postgres
# default database to create
POSTGRES_DB: dspacestatistics
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Set up PostgreSQL
run: |
pg_isready -U postgres -d dspacestatistics
createuser -U postgres dspacestatistics
psql -U postgres -c "ALTER USER dspacestatistics WITH PASSWORD 'dspacestatistics'"
psql -U postgres -d dspacestatistics < tests/dspacestatistics.sql
env:
PGHOST: localhost
PGPASSWORD: postgres
- name: Test with pytest
run: |
pytest
env:
PGHOST: localhost
PGPASSWORD: dspacestatistics

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
__pycache__
venv
*.egg-info

View File

@ -4,16 +4,44 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## 1.4.3 - 2022-03-26
### Updated
- Update dependencies with `poetry update`
- Falcon 3.1.0, a minor change for us, but good to be using a current upstream
version
## 1.4.2 - 2021-04-14
### Updated
- Update dependencies with `poetry update`
- Falcon 3.0.0, a minor change for us, but good to be using a current upstream
version
### Fixed
- Bug in several of the "valid page" tests
### Added
- GitHub Actions workflow to build and test the API
## [1.4.1] - 2021-01-14
### Changed
- Limit Solr query to UUIDs to avoid errors with unmigrated legacy stats (https://github.com/ilri/dspace-statistics-api/issues/12)
### Updated
- Dev dependencies
## [1.4.0] - 2020-12-27
### Added
- indexer.py now indexes views and downloads for communities and collections
- API endpoints for /communities, /community/id, /collections, and /collections/id
- Swagger UI interface on /swagger
- /status page which lists the API version
### Changed
- Add ORDER BY to /items resource to make sure results are returned
deterministically
- Use `fl` parameter in indexer to return only the field we are faceting by
- Minor refactoring of imports for PEP8 style
- More correct calculation of `totalPages` parameter in REST API response
## [1.3.2] - 2020-11-18
### Fixed
@ -69,7 +97,7 @@ and gunicorn 20.0.4
- Minor syntax issues highlighted by flake8
## [1.1.0] - 2019-05-05
## Updated
### Updated
- Falcon 2.0.0 (@alanorth)
## [1.0.0] - 2019-04-15
@ -87,7 +115,7 @@ and gunicorn 20.0.4
## [0.9.0] - 2019-01-22
### Updated
- pytest version 4.0.0
- Fix indexing of sharded statistics cores ([#10))
- Fix indexing of sharded statistics cores (#10)
- Handle case of missing views/downloads gracefully
## [0.8.1] - 2018-11-14

View File

@ -1,8 +1,16 @@
# DSpace Statistics API [![Build Status](https://ci.mjanja.ch/api/badges/alanorth/dspace-statistics-api/status.svg?ref=refs/heads/v6_x)](https://ci.mjanja.ch/alanorth/dspace-statistics-api) [![builds.sr.ht status](https://builds.sr.ht/~alanorth/dspace-statistics-api.svg)](https://builds.sr.ht/~alanorth/dspace-statistics-api?)
<h1 align="center">DSpace Statistics API</h1>
<p align="center">
<a href="https://ci.mjanja.ch/alanorth/dspace-statistics-api"><img alt="Build Status" src="https://ci.mjanja.ch/api/badges/alanorth/dspace-statistics-api/status.svg?ref=refs/heads/v6_x"></a>
<a href="https://github.com/ilri/dspace-statistics-api/actions"><img alt="Build and Test" src="https://github.com/ilri/dspace-statistics-api/actions/workflows/python-app.yml/badge.svg"></a>
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
</p>
DSpace stores item view and download events in a Solr "statistics" core. This information is available for use in the various DSpace user interfaces, but is not exposed externally via any APIs. The DSpace 4/5/6 [REST API](https://wiki.lyrasis.org/display/DSDOC5x/REST+API), for example, only exposes _metadata_ about communities, collections, items, and bitstreams.
- If your DSpace is version 4 or 5, use [dspace-statistics-api v1.1.1](https://github.com/ilri/dspace-statistics-api/releases/tag/v1.1.1)
- If your DSpace is version 6+, use [dspace-statistics-api v1.2.0 or greater](https://github.com/ilri/dspace-statistics-api/releases/tag/v1.2.0)
- Please make sure your statistics have been migrated from integers to UUIDs with the [solr-upgrade-statistics-6x](https://wiki.lyrasis.org/display/DSDOC6x/SOLR+Statistics+Maintenance) command
This project contains an indexer and a [Falcon-based](https://falcon.readthedocs.io/) web application to make the item, community, and collection statistics available via a simple REST API. You can read more about the Solr queries used to gather the item view and download statistics on the [DSpace wiki](https://wiki.lyrasis.org/display/DSPACE/Solr).
@ -119,7 +127,6 @@ The id is the *internal* UUID for an item, community, or collection. You can get
- Use JSON in PostgreSQL
- Add top items endpoint, perhaps `/top/items` or `/items/top`?
- Actually we could add `/items?limit=10&sort=views`
- Add Swagger with OpenAPI 3.0.x with [falcon-swagger-ui](https://github.com/rdidyk/falcon-swagger-ui)
## License
This work is licensed under the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html).

View File

@ -1,6 +1,13 @@
# SPDX-License-Identifier: GPL-3.0-only
import json
import math
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
@ -10,8 +17,53 @@ 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.text = docs_html
class StatusResource:
def on_get(self, req, resp):
message = {"version": VERSION}
resp.status = falcon.HTTP_200
resp.media = message
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:
# 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}]
# Set the version in the schema so Swagger UI can display it
data["info"]["version"] = VERSION
resp.text = json.dumps(data)
class AllStatisticsResource:
@ -29,7 +81,7 @@ class AllStatisticsResource:
with db.cursor() as cursor:
# get total number of communities/collections/items so we can estimate the pages
cursor.execute(f"SELECT COUNT(id) FROM {req.context.statistics_scope}")
pages = round(cursor.fetchone()[0] / limit)
pages = math.ceil(cursor.fetchone()[0] / limit)
# get statistics and use limit and offset to page through results
cursor.execute(
@ -82,7 +134,7 @@ class AllStatisticsResource:
# Helper variables to make working with pages/items/results easier and
# to make the code easier to understand
number_of_elements: int = len(req.context.elements)
pages: int = int(number_of_elements / req.context.limit)
pages: int = math.ceil(number_of_elements / req.context.limit)
first_element: int = req.context.page * req.context.limit
last_element: int = first_element + req.context.limit
# Get a subset of the POSTed items based on our limit. Note that Python
@ -163,19 +215,45 @@ class SingleStatisticsResource:
resp.media = statistics
api = application = falcon.API()
api.add_route("/", RootResource())
app = application = falcon.App()
app.add_route("/", RootResource())
app.add_route("/status", StatusResource())
# Item routes
api.add_route("/items", AllStatisticsResource())
api.add_route("/item/{id_:uuid}", SingleStatisticsResource())
app.add_route("/items", AllStatisticsResource())
app.add_route("/item/{id_:uuid}", SingleStatisticsResource())
# Community routes
api.add_route("/communities", AllStatisticsResource())
api.add_route("/community/{id_:uuid}", SingleStatisticsResource())
app.add_route("/communities", AllStatisticsResource())
app.add_route("/community/{id_:uuid}", SingleStatisticsResource())
# Collection routes
api.add_route("/collections", AllStatisticsResource())
api.add_route("/collection/{id_:uuid}", SingleStatisticsResource())
app.add_route("/collections", AllStatisticsResource())
app.add_route("/collection/{id_:uuid}", SingleStatisticsResource())
# Route to the Swagger UI Openapp schema
app.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 app_url
# parameter.
SWAGGERUI_API_URL = f"{DSPACE_STATISTICS_API_URL}/docs/openapi.json"
register_swaggerui_app(
app,
SWAGGERUI_PATH,
SWAGGERUI_API_URL,
config={
"supportedSubmitMethods": ["get", "post"],
},
uri_prefix=DSPACE_STATISTICS_API_URL,
)
# vim: set sw=4 ts=4 expandtab:

View File

@ -1,3 +1,5 @@
# SPDX-License-Identifier: GPL-3.0-only
import os
# Check if Solr connection information was provided in the environment
@ -9,4 +11,13 @@ DATABASE_PASS = os.environ.get("DATABASE_PASS", "dspacestatistics")
DATABASE_HOST = os.environ.get("DATABASE_HOST", "localhost")
DATABASE_PORT = os.environ.get("DATABASE_PORT", "5432")
# 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.3"
# vim: set sw=4 ts=4 expandtab:

View File

@ -1,3 +1,5 @@
# SPDX-License-Identifier: GPL-3.0-only
import falcon
import psycopg2
import psycopg2.extras

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.3.2</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>

View File

@ -0,0 +1,616 @@
{
"openapi": "3.0.3",
"info": {
"version": "1.4.3",
"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": 0,
"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": 0,
"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"
}
}
}
},
"/status": {
"get": {
"summary": "Get API status",
"operationId": "getStatus",
"tags": [
"status"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"version": {
"type": "string",
"example": "1.4.0-dev"
}
}
}
}
}
},
"405": {
"description": "Method Not Allowed"
}
}
}
}
},
"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"
}
}
}
}
}

View File

@ -1,23 +1,7 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# indexer.py
#
# Copyright 2018 Alan Orth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# ---
#
# Connects to a DSpace Solr statistics core and ingests views and downloads for
# communities, collections, and items into a PostgreSQL database.
#
@ -28,6 +12,8 @@
#
# See: https://wiki.duraspace.org/display/DSPACE/Solr
import math
import psycopg2.extras
import requests
@ -45,7 +31,7 @@ def index_views(indexType: str, facetField: str):
#
# see: https://lucene.apache.org/solr/guide/6_6/the-stats-component.html
solr_query_params = {
"q": "type:2",
"q": f"type:2 AND {facetField}:/.{{36}}/",
"fq": "-isBot:true AND statistics_type:view",
"fl": facetField,
"facet": "true",
@ -75,9 +61,9 @@ def index_views(indexType: str, facetField: str):
exit(0)
# divide results into "pages" (cast to int to effectively round down)
# divide results into "pages" and round up to next integer
results_per_page = 100
results_num_pages = int(results_totalNumFacets / results_per_page)
results_num_pages = math.ceil(results_totalNumFacets / results_per_page)
results_current_page = 0
with DatabaseManager() as db:
@ -92,7 +78,7 @@ def index_views(indexType: str, facetField: str):
)
solr_query_params = {
"q": "type:2",
"q": f"type:2 AND {facetField}:/.{{36}}/",
"fq": "-isBot:true AND statistics_type:view",
"fl": facetField,
"facet": "true",
@ -128,7 +114,7 @@ def index_views(indexType: str, facetField: str):
def index_downloads(indexType: str, facetField: str):
# get the total number of distinct facets for items with at least 1 download
solr_query_params = {
"q": "type:0",
"q": f"type:0 AND {facetField}:/.{{36}}/",
"fq": "-isBot:true AND statistics_type:view AND bundleName:ORIGINAL",
"fl": facetField,
"facet": "true",
@ -158,9 +144,8 @@ def index_downloads(indexType: str, facetField: str):
exit(0)
# divide results into "pages" (cast to int to effectively round down)
results_per_page = 100
results_num_pages = int(results_totalNumFacets / results_per_page)
results_num_pages = math.ceil(results_totalNumFacets / results_per_page)
results_current_page = 0
with DatabaseManager() as db:
@ -175,7 +160,7 @@ def index_downloads(indexType: str, facetField: str):
)
solr_query_params = {
"q": "type:0",
"q": f"type:0 AND {facetField}:/.{{36}}/",
"fq": "-isBot:true AND statistics_type:view AND bundleName:ORIGINAL",
"fl": facetField,
"facet": "true",

View File

@ -1,3 +1,5 @@
# SPDX-License-Identifier: GPL-3.0-only
import requests
from .config import SOLR_SERVER

View File

@ -1,3 +1,5 @@
# SPDX-License-Identifier: GPL-3.0-only
import datetime
import json
import re

939
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,23 @@
[tool.poetry]
name = "dspace-statistics-api"
version = "1.3.2"
description = "A simple REST API to expose Solr view and download statistics for items in a DSpace repository."
version = "1.4.3"
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"
[tool.poetry.dependencies]
python = "^3.6"
gunicorn = "^20.0.4"
falcon = "^2.0.0"
psycopg2-binary = "^2.8.6"
falcon = "3.1.0"
psycopg2 = "^2.9.1"
requests = "^2.24.0"
falcon-swagger-ui = {git = "https://github.com/alanorth/falcon-swagger-ui.git", rev="falcon3-update-swagger-ui"}
[tool.poetry.dev-dependencies]
ipython = { version = "^7.18.1", python = "^3.7" }
flake8 = "^3.8.4"
flake8 = "^4.0.1"
pytest = "^6.1.1"
isort = "^5.5.4"
black = "^20.8b1"
black = {version = "^22.1.0", python = ">=3.6.2"}
isort = {version = "^5.9.1", python = ">=3.6.1"}
[build-system]
requires = ["poetry>=0.12"]

View File

@ -1,244 +1,37 @@
appdirs==1.4.4 \
--hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 \
--hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41
appnope==0.1.2; python_version >= "3.7" and python_version < "4.0" and sys_platform == "darwin" \
--hash=sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442 \
--hash=sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a
atomicwrites==1.4.0; sys_platform == "win32" \
--hash=sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197 \
--hash=sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a
attrs==20.3.0 \
--hash=sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6 \
--hash=sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700
backcall==0.2.0; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255 \
--hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e
black==20.8b1 \
--hash=sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea
certifi==2020.12.5 \
--hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \
--hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c
chardet==4.0.0 \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
click==7.1.2 \
--hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \
--hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a
colorama==0.4.4; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" or sys_platform == "win32" \
--hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \
--hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b
dataclasses==0.6; python_version < "3.7" \
--hash=sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f \
--hash=sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84
decorator==4.4.2; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760 \
--hash=sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7
falcon==2.0.0 \
--hash=sha256:733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983 \
--hash=sha256:f93351459f110b4c1ee28556aef9a791832df6f910bea7b3f616109d534df06b \
--hash=sha256:e9efa0791b5d9f9dd9689015ea6bce0a27fcd5ecbcd30e6d940bffa4f7f03389 \
--hash=sha256:59d1e8c993b9a37ea06df9d72cf907a46cc8063b30717cdac2f34d1658b6f936 \
--hash=sha256:a5ebb22a04c9cc65081938ee7651b4e3b4d2a28522ea8ec04c7bdd2b3e9e8cd8 \
--hash=sha256:95bf6ce986c1119aef12c9b348f4dee9c6dcc58391bdd0bc2b0bf353c2b15986 \
--hash=sha256:aa184895d1ad4573fbfaaf803563d02f019ebdf4790e41cc568a330607eae439 \
--hash=sha256:74cf1d18207381c665b9e6292d65100ce146d958707793174b03869dc6e614f4 \
--hash=sha256:24adcd2b29a8ffa9d552dc79638cd21736a3fb04eda7d102c6cebafdaadb88ad \
--hash=sha256:18157af2a4fc3feedf2b5dcc6196f448639acf01c68bc33d4d5a04c3ef87f494 \
--hash=sha256:e3782b7b92fefd46a6ad1fd8fe63fe6c6f1b7740a95ca56957f48d1aee34b357 \
--hash=sha256:9712975adcf8c6e12876239085ad757b8fdeba223d46d23daef82b47658f83a9 \
--hash=sha256:54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53 \
--hash=sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc
flake8==3.8.4 \
--hash=sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839 \
--hash=sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b
gunicorn==20.0.4 \
--hash=sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c \
--hash=sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626
idna==2.10 \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6
importlib-metadata==3.3.0; python_version < "3.8" \
--hash=sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450 \
--hash=sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed
iniconfig==1.1.1 \
--hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
--hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
ipython==7.19.0; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:c987e8178ced651532b3b1ff9965925bfd445c279239697052561a9ab806d28f \
--hash=sha256:cbb2ef3d5961d44e6a963b9817d4ea4e1fa2eb589c371a470fed14d8d40cbd6a
ipython-genutils==0.2.0; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8 \
--hash=sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8
isort==5.6.4 \
--hash=sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7 \
--hash=sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58
jedi==0.17.2; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5 \
--hash=sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20
mccabe==0.6.1 \
--hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
--hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
mypy-extensions==0.4.3 \
--hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \
--hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8
packaging==20.8 \
--hash=sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858 \
--hash=sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093
parso==0.7.1; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea \
--hash=sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9
pathspec==0.8.1 \
--hash=sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d \
--hash=sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd
pexpect==4.8.0; python_version >= "3.7" and python_version < "4.0" and sys_platform != "win32" \
--hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \
--hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c
pickleshare==0.7.5; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56 \
--hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca
pluggy==0.13.1 \
--hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d \
--hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0
prompt-toolkit==3.0.8; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63 \
--hash=sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c
psycopg2-binary==2.8.6 \
--hash=sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0 \
--hash=sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4 \
--hash=sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db \
--hash=sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5 \
--hash=sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25 \
--hash=sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c \
--hash=sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c \
--hash=sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1 \
--hash=sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2 \
--hash=sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152 \
--hash=sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449 \
--hash=sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859 \
--hash=sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550 \
--hash=sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd \
--hash=sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71 \
--hash=sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4 \
--hash=sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb \
--hash=sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da \
--hash=sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2 \
--hash=sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a \
--hash=sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679 \
--hash=sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf \
--hash=sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b \
--hash=sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67 \
--hash=sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66 \
--hash=sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f \
--hash=sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77 \
--hash=sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94 \
--hash=sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729 \
--hash=sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77 \
--hash=sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52 \
--hash=sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd
ptyprocess==0.6.0; python_version >= "3.7" and python_version < "4.0" and sys_platform != "win32" \
--hash=sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f \
--hash=sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0
py==1.10.0 \
--hash=sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a \
--hash=sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3
pycodestyle==2.6.0 \
--hash=sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367 \
--hash=sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e
pyflakes==2.2.0 \
--hash=sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92 \
--hash=sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8
pygments==2.7.3; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08 \
--hash=sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716
pyparsing==2.4.7 \
--hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \
--hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1
pytest==6.2.1 \
--hash=sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8 \
--hash=sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306
regex==2020.11.13 \
--hash=sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85 \
--hash=sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70 \
--hash=sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee \
--hash=sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5 \
--hash=sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7 \
--hash=sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31 \
--hash=sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa \
--hash=sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6 \
--hash=sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e \
--hash=sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884 \
--hash=sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b \
--hash=sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88 \
--hash=sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0 \
--hash=sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1 \
--hash=sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0 \
--hash=sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512 \
--hash=sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba \
--hash=sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538 \
--hash=sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4 \
--hash=sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444 \
--hash=sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f \
--hash=sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d \
--hash=sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af \
--hash=sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f \
--hash=sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b \
--hash=sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8 \
--hash=sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5 \
--hash=sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b \
--hash=sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c \
--hash=sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683 \
--hash=sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc \
--hash=sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364 \
--hash=sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e \
--hash=sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e \
--hash=sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917 \
--hash=sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b \
--hash=sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9 \
--hash=sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c \
--hash=sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f \
--hash=sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d \
--hash=sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562
requests==2.25.1 \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804
toml==0.10.2 \
--hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
--hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
traitlets==5.0.5; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426 \
--hash=sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396
typed-ast==1.4.1 \
--hash=sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3 \
--hash=sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb \
--hash=sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919 \
--hash=sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01 \
--hash=sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75 \
--hash=sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652 \
--hash=sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7 \
--hash=sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1 \
--hash=sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa \
--hash=sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614 \
--hash=sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41 \
--hash=sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b \
--hash=sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe \
--hash=sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355 \
--hash=sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6 \
--hash=sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907 \
--hash=sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d \
--hash=sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c \
--hash=sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4 \
--hash=sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34 \
--hash=sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b
typing-extensions==3.7.4.3 \
--hash=sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f \
--hash=sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918 \
--hash=sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c
urllib3==1.26.2 \
--hash=sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473 \
--hash=sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08
wcwidth==0.2.5; python_version >= "3.7" and python_version < "4.0" \
--hash=sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784 \
--hash=sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83
zipp==3.4.0; python_version < "3.8" \
--hash=sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108 \
--hash=sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb
atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0"
attrs==21.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
black==22.1.0; python_full_version >= "3.6.2"
certifi==2021.10.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0"
charset-normalizer==2.0.12; python_full_version >= "3.6.0" and python_version >= "3"
click==8.0.4; python_version >= "3.6" and python_full_version >= "3.6.2"
colorama==0.4.4; sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.6.2" and platform_system == "Windows"
dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" and python_full_version >= "3.6.2"
falcon-swagger-ui @ git+https://github.com/alanorth/falcon-swagger-ui.git@falcon3-update-swagger-ui
falcon==3.1.0; python_version >= "3.5"
flake8==4.0.1; python_version >= "3.6"
gunicorn==20.1.0; python_version >= "3.5"
idna==3.3; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5"
importlib-metadata==4.2.0; python_version < "3.8" and python_version >= "3.6" and python_full_version >= "3.6.2"
iniconfig==1.1.1; python_version >= "3.6"
isort==5.10.1; python_full_version >= "3.6.1"
jinja2==3.0.3; python_version >= "3.6"
markupsafe==2.0.1; python_version >= "3.6"
mccabe==0.6.1; python_version >= "3.6"
mypy-extensions==0.4.3; python_full_version >= "3.6.2"
packaging==21.3; python_version >= "3.6"
pathspec==0.9.0; python_full_version >= "3.6.2"
platformdirs==2.4.0; python_version >= "3.6" and python_full_version >= "3.6.2"
pluggy==1.0.0; python_version >= "3.6"
psycopg2==2.9.3; python_version >= "3.6"
py==1.11.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
pycodestyle==2.8.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
pyflakes==2.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
pyparsing==3.0.7; python_version >= "3.6"
pytest==6.2.5; python_version >= "3.6"
requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6"
tomli==1.2.3; python_version >= "3.6" and python_full_version >= "3.6.2"
typed-ast==1.5.2; python_version < "3.8" and implementation_name == "cpython" and python_full_version >= "3.6.2" and python_version >= "3.6"
typing-extensions==4.1.1; python_version < "3.8" and python_full_version >= "3.6.2" and python_version >= "3.6"
urllib3==1.26.9; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4"
zipp==3.6.0; python_version < "3.8" and python_version >= "3.6"

View File

@ -1,66 +1,11 @@
certifi==2020.12.5 \
--hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \
--hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c
chardet==4.0.0 \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
falcon==2.0.0 \
--hash=sha256:733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983 \
--hash=sha256:f93351459f110b4c1ee28556aef9a791832df6f910bea7b3f616109d534df06b \
--hash=sha256:e9efa0791b5d9f9dd9689015ea6bce0a27fcd5ecbcd30e6d940bffa4f7f03389 \
--hash=sha256:59d1e8c993b9a37ea06df9d72cf907a46cc8063b30717cdac2f34d1658b6f936 \
--hash=sha256:a5ebb22a04c9cc65081938ee7651b4e3b4d2a28522ea8ec04c7bdd2b3e9e8cd8 \
--hash=sha256:95bf6ce986c1119aef12c9b348f4dee9c6dcc58391bdd0bc2b0bf353c2b15986 \
--hash=sha256:aa184895d1ad4573fbfaaf803563d02f019ebdf4790e41cc568a330607eae439 \
--hash=sha256:74cf1d18207381c665b9e6292d65100ce146d958707793174b03869dc6e614f4 \
--hash=sha256:24adcd2b29a8ffa9d552dc79638cd21736a3fb04eda7d102c6cebafdaadb88ad \
--hash=sha256:18157af2a4fc3feedf2b5dcc6196f448639acf01c68bc33d4d5a04c3ef87f494 \
--hash=sha256:e3782b7b92fefd46a6ad1fd8fe63fe6c6f1b7740a95ca56957f48d1aee34b357 \
--hash=sha256:9712975adcf8c6e12876239085ad757b8fdeba223d46d23daef82b47658f83a9 \
--hash=sha256:54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53 \
--hash=sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc
gunicorn==20.0.4 \
--hash=sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c \
--hash=sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626
idna==2.10 \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6
psycopg2-binary==2.8.6 \
--hash=sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0 \
--hash=sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4 \
--hash=sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db \
--hash=sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5 \
--hash=sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25 \
--hash=sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c \
--hash=sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c \
--hash=sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1 \
--hash=sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2 \
--hash=sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152 \
--hash=sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449 \
--hash=sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859 \
--hash=sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550 \
--hash=sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd \
--hash=sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71 \
--hash=sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4 \
--hash=sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb \
--hash=sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da \
--hash=sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2 \
--hash=sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a \
--hash=sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679 \
--hash=sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf \
--hash=sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b \
--hash=sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67 \
--hash=sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66 \
--hash=sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f \
--hash=sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77 \
--hash=sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94 \
--hash=sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729 \
--hash=sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77 \
--hash=sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52 \
--hash=sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd
requests==2.25.1 \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804
urllib3==1.26.2 \
--hash=sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473 \
--hash=sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08
certifi==2021.10.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0"
charset-normalizer==2.0.12; python_full_version >= "3.6.0" and python_version >= "3"
falcon-swagger-ui @ git+https://github.com/alanorth/falcon-swagger-ui.git@falcon3-update-swagger-ui
falcon==3.1.0; python_version >= "3.5"
gunicorn==20.1.0; python_version >= "3.5"
idna==3.3; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5"
jinja2==3.0.3; python_version >= "3.6"
markupsafe==2.0.1; python_version >= "3.6"
psycopg2==2.9.3; python_version >= "3.6"
requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
urllib3==1.26.9; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,379 @@
# SPDX-License-Identifier: GPL-3.0-only
import json
from unittest.mock import patch
import pytest
from falcon import testing
from dspace_statistics_api.app import app
@pytest.fixture
def client():
return testing.TestClient(app)
def test_get_collection(client):
"""Test requesting a single collection."""
response = client.simulate_get("/collection/8ea4b611-1f59-4d4e-b78d-a9921a72cfe7")
response_doc = json.loads(response.text)
assert isinstance(response_doc["downloads"], int)
assert isinstance(response_doc["id"], str)
assert isinstance(response_doc["views"], int)
assert response.status_code == 200
def test_get_missing_collection(client):
"""Test requesting a single non-existing collection."""
response = client.simulate_get("/collection/508abe0a-689f-402e-885d-2f6b02e7a39c")
assert response.status_code == 404
def test_get_collections(client):
"""Test requesting 100 collections."""
response = client.simulate_get("/collections", query_string="limit=100")
response_doc = json.loads(response.text)
assert isinstance(response_doc["currentPage"], int)
assert isinstance(response_doc["totalPages"], int)
assert isinstance(response_doc["statistics"], list)
assert response.status_code == 200
def test_get_collections_invalid_limit(client):
"""Test requesting 100 collections with an invalid limit parameter."""
response = client.simulate_get("/collections", query_string="limit=101")
assert response.status_code == 400
def test_get_collections_invalid_page(client):
"""Test requesting 100 collections with an invalid page parameter."""
response = client.simulate_get("/collections", query_string="page=-1")
assert response.status_code == 400
@pytest.mark.xfail
def test_post_collections_valid_dateFrom(client):
"""Test POSTing a request to /collections with a valid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00Z",
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_collections_valid_dateFrom_mocked(client):
"""Mock test POSTing a request to /collections with a valid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00Z",
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
get_views_return_value = {
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7": 21,
"260548c8-fda4-4dc8-a979-03495753cdd5": 0,
}
get_downloads_return_value = {
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7": 575,
"260548c8-fda4-4dc8-a979-03495753cdd5": 899,
}
with patch(
"dspace_statistics_api.app.get_views", return_value=get_views_return_value
):
with patch(
"dspace_statistics_api.app.get_downloads",
return_value=get_downloads_return_value,
):
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_collections_invalid_dateFrom(client):
"""Test POSTing a request to /collections with an invalid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00",
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_collections_valid_dateTo(client):
"""Test POSTing a request to /collections with a valid dateTo parameter in the request body."""
request_body = {
"dateTo": "2020-01-01T00:00:00Z",
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_collections_valid_dateTo_mocked(client):
"""Mock test POSTing a request to /collections with a valid dateTo parameter in the request body."""
request_body = {
"dateTo": "2020-01-01T00:00:00Z",
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
get_views_return_value = {
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7": 21,
"260548c8-fda4-4dc8-a979-03495753cdd5": 0,
}
get_downloads_return_value = {
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7": 575,
"260548c8-fda4-4dc8-a979-03495753cdd5": 899,
}
with patch(
"dspace_statistics_api.app.get_views", return_value=get_views_return_value
):
with patch(
"dspace_statistics_api.app.get_downloads",
return_value=get_downloads_return_value,
):
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_collections_invalid_dateTo(client):
"""Test POSTing a request to /collections with an invalid dateTo parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00",
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_collections_valid_limit(client):
"""Test POSTing a request to /collections with a valid limit parameter in the request body."""
request_body = {
"limit": 1,
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 1
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 1
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
def test_post_collections_valid_limit_mocked(client):
"""Mock test POSTing a request to /collections with a valid limit parameter in the request body."""
request_body = {
"limit": 1,
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
get_views_return_value = {"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7": 21}
get_downloads_return_value = {"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7": 575}
with patch(
"dspace_statistics_api.app.get_views", return_value=get_views_return_value
):
with patch(
"dspace_statistics_api.app.get_downloads",
return_value=get_downloads_return_value,
):
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 1
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 1
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
def test_post_collections_invalid_limit(client):
"""Test POSTing a request to /collections with an invalid limit parameter in the request body."""
request_body = {
"limit": -1,
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_collections_valid_page(client):
"""Test POSTing a request to /collections with a valid page parameter in the request body."""
request_body = {
"page": 0,
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert response.json["totalPages"] == 1
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_collections_valid_page_mocked(client):
"""Mock test POSTing a request to /collections with a valid page parameter in the request body."""
request_body = {
"page": 0,
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
get_views_return_value = {
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7": 21,
"260548c8-fda4-4dc8-a979-03495753cdd5": 0,
}
get_downloads_return_value = {
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7": 575,
"260548c8-fda4-4dc8-a979-03495753cdd5": 899,
}
with patch(
"dspace_statistics_api.app.get_views", return_value=get_views_return_value
):
with patch(
"dspace_statistics_api.app.get_downloads",
return_value=get_downloads_return_value,
):
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_collections_invalid_page(client):
"""Test POSTing a request to /collections with an invalid page parameter in the request body."""
request_body = {
"page": -1,
"collections": [
"8ea4b611-1f59-4d4e-b78d-a9921a72cfe7",
"260548c8-fda4-4dc8-a979-03495753cdd5",
],
}
response = client.simulate_post("/collections", json=request_body)
assert response.status_code == 400
# vim: set sw=4 ts=4 expandtab:

View File

@ -0,0 +1,379 @@
# SPDX-License-Identifier: GPL-3.0-only
import json
from unittest.mock import patch
import pytest
from falcon import testing
from dspace_statistics_api.app import app
@pytest.fixture
def client():
return testing.TestClient(app)
def test_get_community(client):
"""Test requesting a single community."""
response = client.simulate_get("/community/bde7139c-d321-46bb-aef6-ae70799e5edb")
response_doc = json.loads(response.text)
assert isinstance(response_doc["downloads"], int)
assert isinstance(response_doc["id"], str)
assert isinstance(response_doc["views"], int)
assert response.status_code == 200
def test_get_missing_community(client):
"""Test requesting a single non-existing community."""
response = client.simulate_get("/item/dec6bfc6-efeb-4f74-8436-79fa80bb5c21")
assert response.status_code == 404
def test_get_communities(client):
"""Test requesting 100 communities."""
response = client.simulate_get("/communities", query_string="limit=100")
response_doc = json.loads(response.text)
assert isinstance(response_doc["currentPage"], int)
assert isinstance(response_doc["totalPages"], int)
assert isinstance(response_doc["statistics"], list)
assert response.status_code == 200
def test_get_communities_invalid_limit(client):
"""Test requesting 100 communities with an invalid limit parameter."""
response = client.simulate_get("/communities", query_string="limit=101")
assert response.status_code == 400
def test_get_communities_invalid_page(client):
"""Test requesting 100 communities with an invalid page parameter."""
response = client.simulate_get("/communities", query_string="page=-1")
assert response.status_code == 400
@pytest.mark.xfail
def test_post_communities_valid_dateFrom(client):
"""Test POSTing a request to /communities with a valid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00Z",
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_communities_valid_dateFrom_mocked(client):
"""Mock test POSTing a request to /communities with a valid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00Z",
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
get_views_return_value = {
"bde7139c-d321-46bb-aef6-ae70799e5edb": 309,
"2a920a61-b08a-4642-8e5d-2639c6702b1f": 0,
}
get_downloads_return_value = {
"bde7139c-d321-46bb-aef6-ae70799e5edb": 400,
"2a920a61-b08a-4642-8e5d-2639c6702b1f": 290,
}
with patch(
"dspace_statistics_api.app.get_views", return_value=get_views_return_value
):
with patch(
"dspace_statistics_api.app.get_downloads",
return_value=get_downloads_return_value,
):
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_communities_invalid_dateFrom(client):
"""Test POSTing a request to /communities with an invalid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00",
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_communities_valid_dateTo(client):
"""Test POSTing a request to /communities with a valid dateTo parameter in the request body."""
request_body = {
"dateTo": "2020-01-01T00:00:00Z",
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_communities_valid_dateTo_mocked(client):
"""Mock test POSTing a request to /communities with a valid dateTo parameter in the request body."""
request_body = {
"dateTo": "2020-01-01T00:00:00Z",
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
get_views_return_value = {
"bde7139c-d321-46bb-aef6-ae70799e5edb": 21,
"2a920a61-b08a-4642-8e5d-2639c6702b1f": 0,
}
get_downloads_return_value = {
"bde7139c-d321-46bb-aef6-ae70799e5edb": 575,
"2a920a61-b08a-4642-8e5d-2639c6702b1f": 899,
}
with patch(
"dspace_statistics_api.app.get_views", return_value=get_views_return_value
):
with patch(
"dspace_statistics_api.app.get_downloads",
return_value=get_downloads_return_value,
):
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_communities_invalid_dateTo(client):
"""Test POSTing a request to /communities with an invalid dateTo parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00",
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_communities_valid_limit(client):
"""Test POSTing a request to /communities with a valid limit parameter in the request body."""
request_body = {
"limit": 1,
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 1
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 1
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
def test_post_communities_valid_limit_mocked(client):
"""Mock test POSTing a request to /communities with a valid limit parameter in the request body."""
request_body = {
"limit": 1,
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
get_views_return_value = {"bde7139c-d321-46bb-aef6-ae70799e5edb": 200}
get_downloads_return_value = {"bde7139c-d321-46bb-aef6-ae70799e5edb": 309}
with patch(
"dspace_statistics_api.app.get_views", return_value=get_views_return_value
):
with patch(
"dspace_statistics_api.app.get_downloads",
return_value=get_downloads_return_value,
):
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 1
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 1
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
def test_post_communities_invalid_limit(client):
"""Test POSTing a request to /communities with an invalid limit parameter in the request body."""
request_body = {
"limit": -1,
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_communities_valid_page(client):
"""Test POSTing a request to /communities with a valid page parameter in the request body."""
request_body = {
"page": 0,
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert response.json["totalPages"] == 1
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_communities_valid_page_mocked(client):
"""Mock test POSTing a request to communities with a valid page parameter in the request body."""
request_body = {
"page": 0,
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
get_views_return_value = {
"bde7139c-d321-46bb-aef6-ae70799e5edb": 21,
"2a920a61-b08a-4642-8e5d-2639c6702b1f": 0,
}
get_downloads_return_value = {
"bde7139c-d321-46bb-aef6-ae70799e5edb": 575,
"2a920a61-b08a-4642-8e5d-2639c6702b1f": 899,
}
with patch(
"dspace_statistics_api.app.get_views", return_value=get_views_return_value
):
with patch(
"dspace_statistics_api.app.get_downloads",
return_value=get_downloads_return_value,
):
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert isinstance(response.json["totalPages"], int)
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
assert isinstance(response.json["statistics"][1]["views"], int)
assert isinstance(response.json["statistics"][1]["downloads"], int)
def test_post_communities_invalid_page(client):
"""Test POSTing a request to /communities with an invalid page parameter in the request body."""
request_body = {
"page": -1,
"communities": [
"bde7139c-d321-46bb-aef6-ae70799e5edb",
"2a920a61-b08a-4642-8e5d-2639c6702b1f",
],
}
response = client.simulate_post("/communities", json=request_body)
assert response.status_code == 400
# vim: set sw=4 ts=4 expandtab:

50
tests/test_api_docs.py Normal file
View File

@ -0,0 +1,50 @@
# SPDX-License-Identifier: GPL-3.0-only
import pytest
from falcon import testing
from dspace_statistics_api.app import app
@pytest.fixture
def client():
return testing.TestClient(app)
def test_get_docs(client):
"""Test requesting the documentation at the root."""
response = client.simulate_get("/")
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
def test_get_status(client):
"""Test requesting the status page."""
response = client.simulate_get("/status")
assert isinstance(response.content, bytes)
assert response.status_code == 200
# vim: set sw=4 ts=4 expandtab:

View File

@ -1,23 +1,17 @@
from falcon import testing
# SPDX-License-Identifier: GPL-3.0-only
import json
import pytest
from unittest.mock import patch
from dspace_statistics_api.app import api
import pytest
from falcon import testing
from dspace_statistics_api.app import app
@pytest.fixture
def client():
return testing.TestClient(api)
def test_get_docs(client):
"""Test requesting the documentation at the root."""
response = client.simulate_get("/")
assert isinstance(response.content, bytes)
assert response.status_code == 200
return testing.TestClient(app)
def test_get_item(client):
@ -70,7 +64,7 @@ def test_get_items_invalid_page(client):
@pytest.mark.xfail
def test_post_items_valid_dateFrom(client):
"""Test POSTing a request with a valid dateFrom parameter in the request body."""
"""Test POSTing a request to /items with a valid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00Z",
@ -94,7 +88,7 @@ def test_post_items_valid_dateFrom(client):
def test_post_items_valid_dateFrom_mocked(client):
"""Mock test POSTing a request with a valid dateFrom parameter in the request body."""
"""Mock test POSTing a request to /items with a valid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00Z",
@ -134,7 +128,7 @@ def test_post_items_valid_dateFrom_mocked(client):
def test_post_items_invalid_dateFrom(client):
"""Test POSTing a request with an invalid dateFrom parameter in the request body."""
"""Test POSTing a request to /items with an invalid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00",
@ -151,7 +145,7 @@ def test_post_items_invalid_dateFrom(client):
@pytest.mark.xfail
def test_post_items_valid_dateTo(client):
"""Test POSTing a request with a valid dateTo parameter in the request body."""
"""Test POSTing a request to /items with a valid dateTo parameter in the request body."""
request_body = {
"dateTo": "2020-01-01T00:00:00Z",
@ -175,7 +169,7 @@ def test_post_items_valid_dateTo(client):
def test_post_items_valid_dateTo_mocked(client):
"""Mock test POSTing a request with a valid dateTo parameter in the request body."""
"""Mock test POSTing a request to /items with a valid dateTo parameter in the request body."""
request_body = {
"dateTo": "2020-01-01T00:00:00Z",
@ -215,7 +209,7 @@ def test_post_items_valid_dateTo_mocked(client):
def test_post_items_invalid_dateTo(client):
"""Test POSTing a request with an invalid dateTo parameter in the request body."""
"""Test POSTing a request to /items with an invalid dateTo parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00",
@ -232,7 +226,7 @@ def test_post_items_invalid_dateTo(client):
@pytest.mark.xfail
def test_post_items_valid_limit(client):
"""Test POSTing a request with a valid limit parameter in the request body."""
"""Test POSTing a request to /items with a valid limit parameter in the request body."""
request_body = {
"limit": 1,
@ -254,7 +248,7 @@ def test_post_items_valid_limit(client):
def test_post_items_valid_limit_mocked(client):
"""Mock test POSTing a request with a valid limit parameter in the request body."""
"""Mock test POSTing a request to /items with a valid limit parameter in the request body."""
request_body = {
"limit": 1,
@ -286,7 +280,7 @@ def test_post_items_valid_limit_mocked(client):
def test_post_items_invalid_limit(client):
"""Test POSTing a request with an invalid limit parameter in the request body."""
"""Test POSTing a request to /items with an invalid limit parameter in the request body."""
request_body = {
"limit": -1,
@ -303,7 +297,7 @@ def test_post_items_invalid_limit(client):
@pytest.mark.xfail
def test_post_items_valid_page(client):
"""Test POSTing a request with a valid page parameter in the request body."""
"""Test POSTing a request to /items with a valid page parameter in the request body."""
request_body = {
"page": 0,
@ -318,7 +312,7 @@ def test_post_items_valid_page(client):
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert response.json["totalPages"] == 0
assert response.json["totalPages"] == 1
assert len(response.json["statistics"]) == 2
assert isinstance(response.json["statistics"][0]["views"], int)
assert isinstance(response.json["statistics"][0]["downloads"], int)
@ -327,7 +321,7 @@ def test_post_items_valid_page(client):
def test_post_items_valid_page_mocked(client):
"""Mock test POSTing a request with a valid page parameter in the request body."""
"""Mock test POSTing a request to /items with a valid page parameter in the request body."""
request_body = {
"page": 0,
@ -367,7 +361,7 @@ def test_post_items_valid_page_mocked(client):
def test_post_items_invalid_page(client):
"""Test POSTing a request with an invalid page parameter in the request body."""
"""Test POSTing a request to /items with an invalid page parameter in the request body."""
request_body = {
"page": -1,
@ -380,3 +374,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: