1
0
mirror of https://github.com/ilri/dspace-statistics-api.git synced 2025-07-01 12:11:58 +02:00

Compare commits

...

261 Commits

Author SHA1 Message Date
787eec20ea CHANGELOG.md: Add note about imports
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-18 22:52:14 +02:00
9e6fcf279b dspace_statistics_api/items.py: Format with black 2020-12-18 22:45:39 +02:00
4dbf734a4b Move all imports to top of file
A few months ago I had an issue setting up mocking because I was
trying to be clever importing these libraries only when I needed
them rather than at the global scope. Someone pointed out to me
that if the imports are at the top of the file Falcon will load
them once when the WSGI server starts, whereas if they are in the
on_get() or on_post() they will load for every request! Also, it
seems that PEP8 recommends keeping imports at the top of the file
anyways, so I will just do that.

Imports sorted with isort.

See: https://www.python.org/dev/peps/pep-0008/#imports
2020-12-18 22:42:06 +02:00
a0d0a47150 items.py: Add fl paramter to Solr queries
I forgot to add the fl parameter here as well.
2020-12-18 16:12:34 +02:00
01e9756cf2 Update requirements
All checks were successful
continuous-integration/drone/push Build is passing
Generated with poetry export:

    $ poetry export -f requirements.txt > requirements.txt
    $ poetry export --dev -f requirements.txt > requirements-dev.txt
2020-12-18 11:20:17 +02:00
b2b4eb2939 poetry.lock: Run poetry update 2020-12-18 11:19:16 +02:00
4bbbaa4af3 dspace_statistics_api/indexer.py: Use fl parameter
All checks were successful
continuous-integration/drone/push Build is passing
I forgot to add the fl parameter to the downloads function.
2020-12-18 10:44:02 +02:00
7e4d5f4b13 README.md: Minor edit to intro 2020-12-18 10:42:48 +02:00
428172854d README.md: Add TODO
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-17 20:44:25 +02:00
2707cb37d5 CHANGELOG.md: Add note about fl parameter
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-17 12:27:11 +02:00
2407aeec70 dspace_statistics_api/indexer.py: Use fl parameter
When indexing item views and downloads the only field we need is the
the id. The `fl` parameter tells Solr which fields to return in the
search results. This should theoretically be more efficient, though
I don't have any time to figure out how to measure it right now.
2020-12-17 12:25:28 +02:00
f3a0e3a671 CHANGELOG.md: Add note about ORDER BY
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-17 10:17:23 +02:00
4590fc8708 dspace_statistics_api/app.py: Use ORDER BY in /items
Since we are paging through the results by limit/offset we need to
be sure that we are returning results deterministically.
2020-12-17 10:10:40 +02:00
8b924cf450 Remove TravisCI config
All checks were successful
continuous-integration/drone/push Build is passing
I will use other CIs since TravisCI changed their business model.
2020-12-15 09:38:51 +02:00
ea24c73a6a .drone.yml: Install gcc for Python 3.9
It appears to be needed to compile typed-ast:

    gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -Iast27/Include -I/usr/local/include/python3.9 -c ast27/Custom/typed_ast.c -o build/temp.linux-x86_64-3.9/ast27/Custom/typed_ast.o
    error: command 'gcc' failed: No such file or directory
    ----------------------------------------
    ERROR: Failed building wheel for typed-ast
2020-12-14 22:50:21 +02:00
cd98d33615 .drone.yml: Only install requirements-dev.txt
It seems that Poetry's --dev export includes both dev and non-dev
libraries so we don't need to install both.
2020-12-14 22:42:00 +02:00
9d112266ca Update requirements-dev.txt
Generated with poetry export:

    $ poetry export --dev -f requirements.txt > requirements-dev.txt
2020-12-14 22:05:18 +02:00
2b067050ff Remove pytest-clarity
It is missing a six dependency which causes the build to fail. I
could simply add six to the virtualenv but it feels dirty. I don't
actually *need* pytest-clarity for anything so I'll just remove it.

See: https://github.com/darrenburns/pytest-clarity/issues/14
2020-12-14 22:05:03 +02:00
dc683f2d1c pytest.ini: Change --strict to --strict-markers
This is deprecated since pytest 6.2.0.

See: https://docs.pytest.org/en/stable/deprecations.html#the-strict-command-line-option
2020-12-14 19:07:02 +02:00
f60f529bd7 Update requirements
Generated with poetry export:

    $ poetry export -f requirements.txt > requirements.txt
    $ poetry export --dev -f requirements.txt > requirements-dev.txt
2020-12-14 15:41:40 +02:00
7db8458201 poetry.lock: Run poetry update
[SKIP CI]
2020-12-14 15:40:06 +02:00
707f878b94 Add .drone.yml
Uses multiple pipelines to test several versions of Python. A few
things to note:

- I use the -slim Python packages, which are smaller and yet still
have no problem installing psycopg2-binary with pip
- I have to start a PostgreSQL database service for each pipeline
separately
2020-12-14 15:38:50 +02:00
930250352a Update docs about POST /items 2020-12-13 20:09:20 +02:00
e27f30ba4d README.md: Use travis-ci.com domain for badge link 2020-12-08 09:11:38 +02:00
28d1917038 README.md: Use travis-ci.com domain for badge 2020-12-08 09:09:19 +02:00
fc6a9c2ad1 tests: Update for real data
Now that CGSpace is running DSpace 6 I will use some real UUIDs to
make things easier in the future.
2020-11-25 14:56:47 +02:00
3125e96a16 Bump version to 1.3.2 2020-11-18 22:01:18 +02:00
66143ff00f 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-11-18 21:59:33 +02:00
2d15f12be9 poetry.lock: Run poetry update 2020-11-18 21:58:32 +02:00
9218039e61 CHANGELOG.md: Add note about limit param 2020-11-18 21:57:12 +02:00
88a8db6c78 Make sure limit is between 1 and 100
We were not properly checking whether the limit was actually less
than or equal to 100.
2020-11-18 21:55:54 +02:00
3995eba0a7 CHANGELOG.md: Add note about Solr bot filter 2020-11-17 17:42:30 +02:00
810508d038 dspace_statistics_api/indexer.py: Use -isBot:true
Minor change to bot filtering. We should use a negated match for
documents that have `isBot:true` rather than looking for documents
that are tagged with `isBot:false` (the distinction is subtle, but
important).
2020-11-17 17:40:08 +02:00
ecafab57cb README.md: Update DSpace version note 2020-11-16 16:16:21 +02:00
9c9431b58c CHANGELOG.md: Add unreleased changes 2020-11-02 22:14:18 +02:00
2d6520fc97 Fix limit in docs 2020-11-02 22:14:08 +02:00
79a393d33f 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-11-02 22:10:29 +02:00
149f6c418f poetry.lock: Run poetry update 2020-11-02 22:00:29 +02:00
ca1582a8b6 Make sure limit is between 1 and 100
We were not properly checking whether the limit was greater than 0
in all cases.
2020-11-02 21:59:20 +02:00
1904c243a4 Revert ".travis.yml: Use Ubuntu 20.04 "Focal" environment"
This reverts commit 0baa07f70a.

Focal only has PostgreSQL 12 installed, and we are not quite there
yet (our production has 9.6, testing has 10).
2020-10-29 00:11:28 +03:00
0baa07f70a .travis.yml: Use Ubuntu 20.04 "Focal" environment 2020-10-29 00:04:47 +03:00
59214ffcb6 .travis.yml: Bump Python versions
Test Python 3.9 now that it was released, and allow tests to fail
on nightly builds.
2020-10-29 00:03:58 +03:00
549b8bf1a7 dspace_statistics_api/docs/index.html: Fix version
We need to print it in the body, not the title.
2020-10-06 22:22:11 +03:00
899a79b2e7 Version 1.3.1 2020-10-06 22:15:52 +03:00
4c59469055 Update requirements
Generated with poetry export:

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

The `--no-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-10-06 22:11:54 +03:00
4e9064329d Bump version to 1.3.0 2020-10-06 21:33:38 +03:00
4958d5d2e9 pyproject.toml: Fix email 2020-10-06 18:46:31 +03:00
923ed0a434 tests/test_api.py: Add tests for /items POST handlers
This adds tests for the new /items POST handler, both with mocked
data and a live connection to a Solr statistics core. Tests that
only work when Solr is available are marked with XFAIL so that they
don't turn the whole test suite red.

In each test I try to assert as many parameters as we can know for
each response so that we cover all expectations. For example, when
we test a valid limit parameter we should test whether the response
not only has the same limit parameter, but that the number of items
has actually been limited and the number of pages has been adjusted
accordingly.

See: https://docs.pytest.org/en/stable/skipping.html
2020-10-06 16:51:53 +03:00
5acd927210 dspace_statistics_api: Sort imports with isort 2020-10-06 15:12:13 +03:00
630fa0d5fb dspace_statistics_api/util.py: Fix f-strings
flake8 raised this warning:

    F541 f-string is missing placeholders
2020-10-06 15:11:12 +03:00
58d2b8d4ed dspace_statistics_api/items.py: Move util import
Move util import from global scope because it causes tests to fail.
We don't need the set up the Solr connection unless we're actually
trying to use the get_views and get_downloads methods, either when
running the API in production or during tests where the connection
has been set up.
2020-10-06 15:07:00 +03:00
e6572d9469 .build.yml: Use poetry instead of pipenv 2020-10-05 22:37:42 +03:00
85fca81611 Update requirements
Generated with poetry export:

    $ poetry export -f requirements.txt > requirements.txt
    $ poetry export --dev -f requirements.txt > requirements-dev.txt
2020-10-05 22:33:27 +03:00
c81a8d03d7 Remove pipenv configuration
I was having issues with re-creating an environment from scratch:

    ModuleNotFoundError: No module named 'virtualenv.seed.via_app_data'

Switching to Poetry for now.
2020-10-05 22:32:01 +03:00
2923a3b325 Add Poetry configuration
I was having some problems with pipenv when trying to install a
clean environment:

    ModuleNotFoundError: No module named 'virtualenv.seed.via_app_data'
2020-10-05 22:30:35 +03:00
d4518d62ad dspace_statistics_api/app.py: Refactor for testability
I thought it was clever to only import these in the on_post handler
because they aren't needed elsewhere, but it turns out that this is
not a common pattern and even causes problems with testability.

First, if the imports are at the top of the file as PEP8 recommends,
then the WSGI server will import them once when it loads the app and
they remain in memory for the lifecycle of the app. If the imports
are in the on_post handler they would be re-imported on every request!

Second, this pattern of importing in a method makes it tricky to use
object patching in mocks.

See: https://www.python.org/dev/peps/pep-0008/#imports
2020-10-05 20:43:50 +03:00
3a98de78e3 dspace_statistics_api/items.py: Remove executable bit
We don't need to execute this on the command line.
2020-10-05 14:33:36 +03:00
b26439daf3 CHANGELOG.md: Add note about Python dependencies 2020-09-27 11:16:37 +03:00
9e898ba54f Update requirements
Generated from pipenv with:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2020-09-27 11:13:38 +03:00
716d65030c Pipfile.lock: Run pipenv update 2020-09-27 11:13:04 +03:00
5a53b57b3b Refactor /items POST handler to use a before hook
This allows us to do the dirty work of parsing, validating, and
setting local variables from the POST parameters outside of the
on_post function. We then share the parameters via the req.context
object. Functionally it is the same, but readability is better
and it's a neat trick that I could use elsewhere.

See: https://falcon.readthedocs.io/en/stable/user/faq.html#how-can-i-pass-data-from-a-hook-to-a-responder-and-between-hooks
2020-09-26 18:40:52 +03:00
3ceb9a6eb0 dspace_statistics_api/items.py: Fix flake8 warning
According to flake8 we need to use a different syntax for strings
with backslash escape sequences:

> As of Python 3.6, a backslash-character pair that is not a valid
> escape sequence now generates a DeprecationWarning. This will
> eventually become a SyntaxError.

The warning was:

    W605 invalid escape sequence '\-'

See: https://www.flake8rules.com/rules/W605.html
2020-09-26 12:22:06 +03:00
946f0749e2 dspace_statistics_api/app.py: Use bounded_stream in on_post
For reasons I don't quite understand, we need to use bounded_stream
in the on_post request handler in order to use simulate_post() with
the testing client in Falcon 2.0.0. Normal runtime operation via
gunicorn does not have any issues with stream.

See: https://github.com/falconry/falcon/issues/1720
See: https://github.com/falconry/falcon/issues/1554
2020-09-26 11:50:57 +03:00
b06651d1ec dspace_statistics_api/indexer.py: Fix Python comment 2020-09-25 13:35:05 +03:00
a0ee181361 dspace_statistics_api/docs/index.html: Fix whitespace 2020-09-25 13:33:45 +03:00
f58c209609 dspace_statistics_api/indexer.py: Update comment
I don't remember why we needed the stats, but it seems that it was
because without them there is no way to know how many results were
returned and therefore no way to know how many pages we'll need to
iterate over. Having the total number allows us to use a limit and
and offset to page through them deterministically.
2020-09-25 13:25:34 +03:00
6dbff1e78f README.md: Capitalize UUID 2020-09-25 13:03:15 +03:00
731226ec15 README.md: Update for POST /items functionality 2020-09-25 13:01:09 +03:00
2201d3df4e dspace_statistics_api/docs/index.html: Minor HTML syntax issue 2020-09-25 12:55:39 +03:00
2c0436f845 Update API docs HTML for /items POST functionality 2020-09-25 12:53:30 +03:00
f1e939481b dspace_statistics_api/items.py: Remove shebang
This was originally a standalone script I was testing interactively.
2020-09-25 12:39:00 +03:00
4d8026a3d0 Add missing dspace_statistics_api/items.py
This was meant to be added with the new /items POST changes.
2020-09-25 12:30:06 +03:00
de1f462ad2 CHANGELOG.md: Add more notes about /items 2020-09-25 12:29:19 +03:00
8a6bbfd527 CHANGELOG.md: Add note about /items 2020-09-25 12:27:33 +03:00
73c71fa8a0 dspace_statistics_api: Add support for date ranges to /items
You can now POST a JSON request to /items with a list of items and
a date range. This allows the possibility to get view and download
statistics for arbitrary items and arbitrary date ranges.

The JSON request should be in the following format:

    {
        "limit": 100,
        "page": 0,
        "dateFrom": "2020-01-01T00:00:00Z",
        "dateTo": "2020-09-09T00:00:00Z",
        "items": [
            "f44cf173-2344-4eb2-8f00-ee55df32c76f",
            "2324aa41-e9de-4a2b-bc36-16241464683e",
            "8542f9da-9ce1-4614-abf4-f2e3fdb4b305",
            "0fe573e7-042a-4240-a4d9-753b61233908"
        ]
    }

The limit, page, and date parameters are all optional. By default
it will use a limit of 100, page 0, and [* TO *] Solr date range.
2020-09-25 12:21:11 +03:00
7a5e14716d CHANGELOG.md: Add note about indexer refactoring 2020-09-24 12:08:21 +03:00
21b500b4f7 dspace_statistics_api/util.py: Use docstring for get_statistics_shards
It seems better to use a docstring instead of a comment because it
can potentially be used by IDEs or documentation generators.
2020-09-24 12:07:31 +03:00
495386856b Refactor indexer
Move the get_statistics_shards() method to a utility module so it
can be used by other things.
2020-09-24 12:03:12 +03:00
8e87f80e9a dspace_statistics_api/indexer.py: Remove duplicate solr_url variable
This is declared twice and it never changes.
2020-09-24 11:54:31 +03:00
c4bf8bf698 README.md: Add TODO note about sorting by views or downloads 2020-09-24 11:53:23 +03:00
6ff95bb5f2 dspace_statistics_api/indexer.py: Remove SolrClient reference
We stopped using SolrClient in favor of vanilla requests.
2020-09-24 11:30:31 +03:00
0c8fb21f80 README.md: Update DSpace wiki URLs 2020-04-13 15:25:17 +03:00
b359c2466f .travis.yml: Don't build in a container
I didn't realize these LXD containers are not available on AMD64.
Now I understand why the build was so slow: because it was ARM64!
2020-03-29 16:36:47 +03:00
0eaed3e8c4 .travis.yml: Use Python 3.8-dev instead of master
See: https://docs.travis-ci.com/user/languages/python/#specifying-python-versions
2020-03-29 16:26:57 +03:00
70e96214c8 .travis.yml: Go→Python
Fix incorrect language.
2020-03-29 16:24:39 +03:00
cab9f16dbc .travis.yml: Try to run in an LXD container
According to the build environment documentation we need to specify
an OS of Linux in order to get a container instead of a VM.

See: https://docs.travis-ci.com/user/reference/overview/
2020-03-29 16:24:00 +03:00
bd49e1d1f6 .travis.yml: Correctly specify PostgreSQL 10
See: https://docs.travis-ci.com/user/database-setup/#postgresql
2020-03-29 16:19:08 +03:00
144ed9a7c4 .travis.yml: Use PostgreSQL 10.0
Production is still PostgreSQL 9.6, but I have been using 10.0 in
local development and staging environments.
2020-03-29 16:08:29 +03:00
48eef8c8e3 .travis.yml: Test on Python master
But allow failures!
2020-03-29 16:07:44 +03:00
fa9325e8a3 CHANGELOG.md: Add changes for v1.2.1 2020-03-02 14:32:07 +02:00
998e833470 dspace_statistics_api/docs/index.html: Adjust help text 2020-03-02 14:30:16 +02:00
dd8252601f README.md: Adjust API help text 2020-03-02 14:29:13 +02:00
9a9555853f README.md: Add note about versions 2020-03-02 14:28:22 +02:00
385e92cc5e README.md: Update
Remove TODOs that I've recently completed and update introduction.
2020-03-02 14:25:47 +02:00
b0e6481961 tests/dspacestatistics.sql: Update
New database snapshot that uses UUIDs.
2020-03-02 12:36:06 +02:00
f96a903be3 README.md: Update Python requirement 2020-03-02 11:47:03 +02:00
fcf8fa4c29 CHANGELOG.md: Minor syntax and spelling changes 2020-03-02 11:45:26 +02:00
5dd50ff998 CHANGELOG.md: Version 1.2.0
This version only works with DSpace 6+ where the internal item id-
entifiers are UUIDs instead of integers. Version 1.1.1 was the last
version to work with DSpace 4 and 5.
2020-03-02 11:34:58 +02:00
6704e7375f CHANGELOG.md: Add note about Python dependencies 2020-03-02 11:34:13 +02:00
37630d8dac CHANGELOG.md: Add note about DSpace 6+ UUIDs 2020-03-02 11:27:10 +02:00
0ef071a91d dspace_statistics_api: Use f-strings instead of format()
We had previously been avoiding the f-strings because we needed to
run on Python 3.5 and they were only available in Python 3.6+, but
now the black formatter requires Python 3.6 and all our systems are
running Python 3.6+ anyways.
2020-03-02 11:24:29 +02:00
9e7dd28156 dspace_statistics_api/app.py: Use parameterized SQL queries
This is a better way to run SQL queries because psycopg2 takes care
of the quoting for us.
2020-03-02 11:16:05 +02:00
60e6ea57b1 tests/test_api.py: Use UUID
DSpace 6+ uses a UUID for item identifiers instead of an integer so
we need to adapt our tests accordingly. The Python UUID object must
be cast to a string to use it elsewhere in the code.
2020-03-02 11:10:41 +02:00
5955868b9a dspace_statistics_api/app.py: Use UUID
DSpace 6+ uses a UUID for item identifiers instead of an integer so
we need to adapt our PostgreSQL queries to use those. Note that we
can no longer sort results in the "all items" endpoint by ID. Also,
we need to use parameterized psycopg2 queries instead of strings to
support queries with UUIDs properly. To use the Python UUID objects
elsewhere in the code we need to make sure that we cast them to str.
2020-03-02 11:06:48 +02:00
250fd8164f dspace_statistics_api/indexer.py: Use UUID
DSpace 6+ uses a UUID for item identifiers instead of an integer so
we need to update the PostgreSQL schema accordingly. Solr still re-
fers to them as "id" in its schema so we don't need to change anyt-
hing there.
2020-03-01 21:22:10 +02:00
82be1a4d00 Update requirements
Generated from pipenv with:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2020-03-01 21:21:13 +02:00
0615064e3d Add pytest-clarity to pipenv
Makes pytest output easier to understand.
2020-03-01 21:19:28 +02:00
76be1b749a Run pipenv update 2020-03-01 21:13:32 +02:00
92146fe426 tests/test_api.py: Format with black 2019-12-14 12:39:58 +02:00
440b2f2dfa Pipfile.lock: Run pipenv update 2019-12-14 12:38:11 +02:00
67bc30ead0 Pipfile: Specify exact version of black
Black only releases pre-release versions, which causes issues with
pipenv. Instead of always running pipenv with "--pre" and potenti-
ally letting in some other pre-release versions for other depende-
ncies, I would rather specify the latest black version explicitly.

See: https://github.com/psf/black/issues/517
See: https://github.com/microsoft/vscode-python/issues/5171
2019-12-14 12:37:10 +02:00
142959acdb CHANGELOG.md: Unreleased changes 2019-11-27 12:56:39 +02:00
322f5a8db8 .travis.yml: Remove Python 3.5
black does not work with Python 3.5. It's not such a big deal, as
this is only required for running tests, not for running the app.
2019-11-27 12:55:34 +02:00
90dcaa6ec6 CHANGELOG.md: Fix typo 2019-11-27 12:47:07 +02:00
9aca827d69 Update requirements-dev.txt
Generated with pipenv:

    $ pipenv lock -r -d > requirements-dev.txt
2019-11-27 12:36:05 +02:00
1b394ec50e CHANGELOG.md: Move unreleased changes to 1.1.1 2019-11-27 12:32:54 +02:00
3e9753b600 CHANGELOG.md: Add unreleased changes 2019-11-27 12:32:16 +02:00
cb3c3d37fa Sort imports with isort 2019-11-27 12:31:04 +02:00
4ff1fd4a22 Format code with black 2019-11-27 12:30:06 +02:00
d2fe420a9a Add configuration for isort and black
This does linting and automatic code formatting according to PEP8.

See: https://sourcery.ai/blog/python-best-practices/
2019-11-27 12:26:55 +02:00
3197b79578 CHANGELOG.md: Update unreleased changes 2019-11-27 12:14:49 +02:00
eeb8e6bba1 dspace_statistics_api/indexer.py: Fix minor issues raised by flake8 2019-11-27 12:12:05 +02:00
3540ce328b Update requirements
Generated from pipenv with:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2019-11-27 12:08:32 +02:00
520e04f9be Pipfile.lock: run pipenv update
Brings gunicorn 20.0.4, pytest 5.3.1, and others. I hadn't noticed
that gunicorn was bumped from 19.x.x to 20.x.x last week.

See: https://docs.gunicorn.org/en/stable/news.html#id6
2019-11-27 12:06:09 +02:00
8a46a64cfc CHANGELOG.md: Use Python 3.8 for pipenv 2019-11-27 10:53:38 +02:00
b8442f8cce .travis.yml: Remove pipenv-specific environment variables 2019-11-15 00:48:57 +02:00
95f7871cc1 .travis.yml: Use vanilla pip 2019-11-15 00:46:58 +02:00
3bc07027e5 .travis.yml: Test with Python 3.8 2019-11-15 00:46:04 +02:00
afcc445855 Update requirements
Generated from pipenv with:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2019-11-15 00:41:12 +02:00
494548c691 Use Python 3.8.0 for pipenv
Python 3.8.0 was released several months ago and has made it into
Arch Linux's core repositories so it's time to start moving.
2019-11-15 00:38:45 +02:00
feb60b6adf CHANGELOG.md: Update unreleased changes 2019-11-15 00:06:49 +02:00
1541ae3e3b .travis.yml: Use Ubuntu 18.04 "Bionic" 2019-11-14 23:57:46 +02:00
1aedc0ca29 CHANGELOG.md: Add note about Python dependencies 2019-08-29 00:31:31 +03:00
a648183f35 Update requirements
Generated from pipenv with:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2019-08-29 00:31:06 +03:00
b8f379e7fa Pipfile.lock: Run pipenv update
This brings in, among others, psycogpg 2.8.3, requests 2.22.0, and
pytest 5.1.1.
2019-08-29 00:30:06 +03:00
78f9949ecb CHANGELOG.md: Release version 1.1.0 2019-05-05 23:38:04 +03:00
af80c4b447 CHANGELOG.md: Add falcon 2.0.0 to unreleased changes 2019-05-03 16:33:00 +03:00
edd9e90f59 Update requirements
Generated using pipenv:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2019-05-03 16:32:17 +03:00
1806d50a51 Pipfile: Use falcon 2.0.0
See: https://github.com/falconry/falcon/releases/tag/2.0.0
2019-05-03 16:31:06 +03:00
a459e66fd9 Use falcon 2.0.0rc2 2019-04-18 10:04:43 +03:00
5a3b392a1d dspace_statistics_api/app.py: Fix Falcon 2.0 syntax
See: dspace_statistics_api/app.py
2019-04-18 09:57:18 +03:00
9dcda114c6 Bump Falcon version to 2.0.0b1
See: https://github.com/falconry/falcon/releases/tag/2.0.0b1
2019-04-18 09:57:18 +03:00
2b8aba5835 CHANGELOG.md: Move unreleased changes to v1.0.0 2019-04-15 10:39:48 +03:00
9eb30a98e3 Update requirements
Generated using pipenv:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2019-04-15 10:31:19 +03:00
622e9a86f1 CHANGELOG.md: Add notes about Python updates 2019-04-15 10:30:29 +03:00
2acd08e0ab Use one-based paging in indexer output
It is easier for humans to understand one-based paging output like
"page 1 of 3" than "page 0 of 2" in the indexer.
2019-04-15 10:25:54 +03:00
f75bcf292c README.md: Remove TODO about SolrClient
I switched to using the vanilla requests library.
2019-04-15 10:24:24 +03:00
8f46ceb8d8 Refactor to use vanilla requests library
The SolrClient library is unmaintained, which is starting to cause
problems due to the moving Python ecosystem. Switching to requests
does not change my code in any meaningful way and makes maintenance
easier.
2019-04-15 10:19:50 +03:00
18e1e1a227 README.md: Add TODO about checking IDs in the database
Theoretically some items could be deleted and we should remove them
from the database.
2019-04-04 18:33:45 +03:00
fd46041698 README.md: Add build badge for sourcehut (sr.ht) 2019-03-17 23:45:33 +02:00
4ce7231ece CHANGELOG.md: Add unreleased changes 2019-03-17 23:40:51 +02:00
60689d9014 Disable emojis and animated output in CI
Makes for cleaner logs.

See: https://docs.travis-ci.com/user/environment-variables/
See: https://man.sr.ht/builds.sr.ht/manifest.md
2019-03-17 23:39:38 +02:00
7bca32189a .travis.yml: Use PostgreSQL 9.6
This matches what we're using in production.
2019-03-17 23:28:06 +02:00
94c5d91d3c CHANGELOG.md: Add unreleased changes 2019-03-17 22:51:39 +02:00
a640f734c8 Pipfile.lock: run pipenv update 2019-03-17 22:46:39 +02:00
d56a3420f7 README.md: Add TODO about SolrClient
SolrClient works, but hasn't been updated in some time and this is
starting to cause issues with some of its dependencies (kazoo). We
can probably get by with using Python requests library and getting
JSON directly from Solr.
2019-02-19 13:54:34 -08:00
7add0d6164 README.md: Add TODO about top items endpoint
This might be something useful that would be trivial to provide from
the data we already have in PostgreSQL.
2019-02-10 14:20:09 +02:00
c86bec4d8f .travis.yml: Use Ubuntu 16.04 xenial image
This is a newer userland and allows us to use Python 3.7, for example.

See: https://docs.travis-ci.com/user/reference/xenial/
2019-02-07 17:41:36 +02:00
5429fe5cc8 Update requirements
Generated from pipenv with:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2019-02-07 17:39:50 +02:00
f8a4cfd3da CHANGELOG.md: Add notes about updated python modules 2019-02-07 17:30:08 +02:00
be94c94433 Pipfile.lock: Run pipenv update 2019-02-07 17:29:47 +02:00
ba49b78a25 CHANGELOG.md: Add build configuration for build.sr.ht
See: https://man.sr.ht/builds.sr.ht/
2019-02-07 17:28:41 +02:00
842f80036f .build.yml: Fix PostgreSQL import
When building on sr.ht the default environment is the home directory
so we need to change to the source directory before trying to import
the SQL file.
2019-02-07 17:25:19 +02:00
f738b8029b Rename sr.ht build.yml to .build.yml
This means git.sr.ht will trigger builds automatically on push.

See: https://man.sr.ht/builds.sr.ht/
2019-02-07 17:09:48 +02:00
d08c43f3d5 build.yml: Functioning build
Finally got this working after testing the manifest manually a few
times on the web UI.
2019-02-07 17:09:48 +02:00
819f8e6b0d Add build.yml for sr.ht
Trying to figure out how to run builds on this new platform.

See: https://man.sr.ht/builds.sr.ht/#build-manifests
2019-02-07 17:09:48 +02:00
c79e50a364 README.md: Add TODO about DSpace 6 UUIDs
I'm not sure how this will affect us, especially if we want to keep
support for DSpace 4, 5, and 6 in the same code base. At least the
REST API endpoint will have to change from an integer, our database
schema will have to change depending on whether the repository is
using IDs or UUIDs, and maybe even the Solr queries will change.
2019-02-07 16:52:36 +02:00
71006d8bbf README.md: Add citation 2019-01-23 16:19:58 +02:00
b7d723ef7c README.md: Fix sentence 2019-01-22 14:23:13 +02:00
914ec52fbb CHANGELOG.md: Move unrelease changes to 0.9.0 2019-01-22 09:02:29 +02:00
5524066656 CHANGELOG.md: Add note about catching errors 2019-01-22 09:01:54 +02:00
043d897cef dspace_statistics_api/indexer.py: Catch case of no views/downloads
Don't fail with an exception when there are no views or downloads,
for example on a new DSpace installation.
2019-01-22 09:00:22 +02:00
bd28353cda README.md: Remove TODO for fixing querying of shards 2019-01-22 08:41:39 +02:00
e23d66c2a2 CHANGELOG.md: Add note about fixing querying of sharded cores 2019-01-22 08:41:31 +02:00
40e284dac0 dspace_statistics_api/indexer.py: Query multiple shards
DSpace's stats-util script splits the Solr statistics core into yearly
shards. We need to use Solr's `shards` query parameter in order to get
the statistics for previous years. This commit adds a helper function
to enumerate the active Solr cores to find yearly shards matching the
statistics-YYYY pattern and add them to the query.
2019-01-22 08:39:36 +02:00
934fa9db9b README.md: Add TODO about sharded statistics cores 2019-01-21 12:55:43 +02:00
1fabb72b58 Update requirements
Generated from pipenv with:

  $ pipenv lock -r > requirements.txt
  $ pipenv lock -r -d > requirements-dev.txt
2019-01-16 12:34:50 +02:00
c7f95f0b60 README.md: Update TODO
I think it might be possible to compute community and collection
statistics from Solr and make them available at new endpoints:

  - /communities
  - /community/id
  - /collections
  - /collection/id
2019-01-16 09:59:29 +02:00
c95a98dd2d Pipfile.lock: update dependencies
Updated with `pipenv update`.
2019-01-15 10:22:46 +02:00
3f70f94a10 Pipfile.lock: Run pipenv update 2018-11-26 11:53:37 +02:00
9b8ad9defd Merge pull request #9 from ilri/pipenv-update
Pipenv update
2018-11-19 23:50:44 +02:00
d69ab20220 CHANGELOG.md: pytest version 4.0.0 2018-11-19 23:46:03 +02:00
378f56ddc2 Pipfile.lock: Run pipenv update 2018-11-19 23:34:34 +02:00
5a2a7d684c CHANGELOG.md: Move unreleased changes to version 0.8.1 2018-11-14 09:37:00 +02:00
18276e910f CHANGELOG.md: Add notes about pipenv 2018-11-14 09:36:13 +02:00
8de8c2765f Merge pull request #8 from ilri/update-dependencies
Update dependencies
2018-11-14 09:34:45 +02:00
11a1755e59 Update requirements.txt
Generated from pipenv with:

    $ pipenv lock -r > requirements.txt
2018-11-14 09:19:47 +02:00
a835b0fdc5 Re-create pipenv environment from scratch
When I originally created the pipenv environment I used the standard
pip requirements.txt that I already had, which captured all the mod-
ules and their exact versions at the time. This makes it hard to se-
parate the project's actual dependencies from the dependencies' dep-
endencies, complicating the Pipfile and making it hard to update mo-
dule versions later.

I've re-created the environment with the following commands:

    $ pipenv install gunicorn falcon psycopg2-binary git+https://github.com/alanorth/SolrClient.git@kazoo-2.5.0#egg=SolrClient
    $ pipenv install --dev ipython flake8 pytest
2018-11-14 09:07:32 +02:00
a88600c92b README.md: Add note about GPLv3 2018-11-13 12:34:31 +02:00
019d9242c9 Merge pull request #7 from ilri/use-pip
Rework to use pip instead of pipenv
2018-11-12 09:17:16 +02:00
f4d7312a3f CHANGELOG.md: Add unreleased changes 2018-11-12 09:02:04 +02:00
9c46cfc7e2 Use Python 3.7 for pipenv
Now that I'm only using pipenv locally it shouldn't create problems
for people. They can still just create a vanilla virtualenv and use
pip to install the dependencies.
2018-11-12 08:54:54 +02:00
c1c2e319ac README.md: Rework to use pip instead of pipenv
Pipenv is great for local development, but I don't think many people
are using it yet. I can use it locally and on Travis, but still keep
vanilla requirements.txt for use with pip. The requirements.txt file
can be generated easily from pipenv itself:

    $ pipenv lock -r > requirements.txt

The same for the development requirements:

    $ pipenv lock -r -d > requirements-dev.txt
2018-11-12 08:49:02 +02:00
0895b4f469 Add requirements-dev.txt for pip
Generated with pipenv lock -r -d. Will be used for separating the
development dependencies.
2018-11-12 08:48:45 +02:00
dcfef06a65 Pipfile.lock: Run pipenv update 2018-11-12 08:20:47 +02:00
13736d6359 CHANGELOG.md: Move unreleased changes to verison 0.8.0 2018-11-11 17:16:26 +02:00
4fc64edeb8 Merge pull request #6 from ilri/pytest
Pytest
2018-11-11 17:14:49 +02:00
2a8901dc4f CHANGELOG.md: Update notes 2018-11-11 17:10:45 +02:00
e25c974796 README.md: We have tests now 2018-11-11 17:08:51 +02:00
ffc62e9ee6 tests/test_api.py: Use response.text for all json.loads()
This allows the code to work in Python 3.5 as well as 3.6+.
2018-11-11 17:05:31 +02:00
556c5ae088 tests/test_api.py: Use response.text instead of content
Falcon's response content is raw bytes, while its text is a string.
Let's use the latter so we can use json.loads() in Python 3.5, 3.6,
and 3.7 with the same code.

See: https://falcon.readthedocs.io/en/stable/api/testing.html
2018-11-11 17:01:17 +02:00
d94134f80a tests/test_api.py: Try to add workaround for Python 3.5
In Python 3.5 it seems that json.loads() cannot decode a bytes, but
it works in Python 3.6 and 3.7. Let's try a workaround to see if we
can get it working on both Python 3.5 and 3.6+.

See: https://docs.python.org/3.5/library/json.html#json.loads
See: https://docs.python.org/3.6/library/json.html#json.loads
2018-11-11 17:00:20 +02:00
586231eb2d .travis.yml: Use PostgreSQL 9.5
Default PostgreSQL in Travis CI is 9.2 which is very old, so let's
try to use 9.5.

See: https://docs.travis-ci.com/user/database-setup/#postgresql
2018-11-11 16:41:35 +02:00
766b77a3b6 .travis.yml: Use PostgreSQL directly
It seems that Travis CI already has a PostgreSQL service running.
2018-11-11 16:35:28 +02:00
1959e8154e .travis.yml: Use localhost for Docker's PostgreSQL ports
See: https://docs.travis-ci.com/user/docker/
2018-11-11 16:28:18 +02:00
d40b2f0b2e Test API using pytest and PostgreSQL on Travis
First attempt at getting the Travis Docker setup correct. Inspired
by the Travis pipenv setup used in Responder.

See: https://docs.travis-ci.com/user/docker/
See: https://github.com/kennethreitz/responder/blob/master/.travis.yml
2018-11-11 16:25:16 +02:00
061d0a8f5f CHANGELOG.md: Add API tests to unreleased changes 2018-11-11 16:24:54 +02:00
e57660ff88 Add initial pytest configuration
From: https://github.com/kennethreitz/responder/blob/master/pytest.ini
2018-11-11 16:24:54 +02:00
5c8756bede Add pytest to pipenv development packages 2018-11-11 16:24:54 +02:00
bae9fb80e4 Add initial API tests
Test the basic assumptions of the API like response codes and types.
2018-11-11 16:24:54 +02:00
8a65d99e08 .travis.yml: Don't limit builds to master
This is good in theory but it means we can't trigger builds for other
branches on the fly from the Travis web interface.
2018-11-11 16:21:48 +02:00
d479b7dc6c CHANGELOG.md: Syntax fixes 2018-11-11 00:08:44 +02:00
40aac8bf89 Merge pull request #5 from ilri/database-error-handling
Database error handling
2018-11-11 00:07:25 +02:00
53ba6f2936 CHANGELOG.md: Add database try/except to unreleased changes 2018-11-11 00:05:49 +02:00
140cc4cb07 README.md: Remove TODO for database try/except
Now database connection errors are properly excepted and raised.
2018-11-11 00:04:28 +02:00
d5d2d2149b dspace_statistics_api/database.py: Raise HTTP 500 on error
Properly except on database connection error and raise an HTTP 500
instead of spamming the console/log with twenty lines of text.
2018-11-10 23:58:58 +02:00
4c51d12eb4 CHANGELOG.md: Move unreleased changes to version 0.7.0 2018-11-07 17:55:01 +02:00
a6ce44e852 Merge pull request #4 from ilri/database-refactor
Database refactor
2018-11-07 17:54:04 +02:00
f6e866a589 dspace_statistics_api/indexer.py: Remove debug code 2018-11-07 17:51:24 +02:00
eb5c187d41 CHANGELOG.md: Add note about database re-factor 2018-11-07 17:50:46 +02:00
b06c82bb16 README.md: Remove TODO about closing database connection
Now I'm using a database manager class with Python's "with" context
blocks to automatically and concisely open and close connections.
2018-11-07 17:47:59 +02:00
2f342be948 Refactor database code to use a context manager
Instead of opening one global persistent database connection when
the application I am now abstracting it to a class that I can use
in combination with Python's "with" context. Both connections and
cursors are kept for the context of each "with" block and closed
automatically when exiting.

See: https://alysivji.github.io/managing-resources-with-context-managers-pythonic.html
See: http://initd.org/psycopg/docs/connection.html#connection.close
2018-11-07 17:41:21 +02:00
e39f2b260c Merge pull request #3 from ilri/ipython
Add ipython to pipenv dev packages
2018-11-07 17:09:09 +02:00
60ad474b88 Add ipython to pipenv dev packages
This is very useful for debugging Python code interactively.
2018-11-07 17:07:14 +02:00
888f85d19e README.md: Adjust installation for pipenv
It's nicer to manager module versions using pipenv, and I can still
generate a requirements.txt for deploying the exact versions on the
production server.
2018-11-04 16:07:27 +02:00
df7de93964 CHANGELOG.md: Add pipenv to unreleased changes 2018-11-04 15:59:11 +02:00
7218631cc4 requirements.txt: Regenerate
Created from pipenv with the following command:

    $ pipenv lock -r > requirements.txt
2018-11-04 15:58:31 +02:00
085e525b2f Regenerate pipenv
Uses 'kazoo-2.5.0' branch name for installing SolrClient instead of
the commit hash and adds flake8 as a dev package. This means that I
can track dependencies for production and development and still end
up with a requirements.txt for produciton.
2018-11-04 15:55:28 +02:00
e1580df12f requirements.txt: Update packages
Generated from pipenv with:

    $ pipenv run pip freeze > requirements.txt
2018-11-04 15:39:09 +02:00
be18779ff9 Add flake8 to pipenv 2018-11-04 15:38:51 +02:00
60cfd8f23b Add Pipfile for pipenv
Eventually I'd like to be able to use pipenv instead of plain pip.
For now I'll just keep using pipenv and generating requirements.txt
like this:

    $ pipenv run pip freeze > requirements.txt

Then I can kinda have the best of both worlds, where I use pipenv
on my local machine and pip with requirements.txt on the server.
2018-11-04 15:35:47 +02:00
87fd117d77 .hound.yml: Set pull requests to failed if build fails 2018-11-04 00:53:37 +02:00
f262ebdca2 CHANGELOG.md: Add unreleased changes 2018-11-04 00:50:46 +02:00
64d7f1a3b2 Enable Flake8 validation in Hound CI
Will check all pull requests in the project to make sure they don't
violate PEP 8 style (except the E501 for long lines because I think
it makes code hard to read).
2018-11-04 00:48:06 +02:00
a238a727d2 CHANGELOG.md: Add unreleased changes 2018-11-04 00:05:16 +02:00
cc5ce3ab98 Correct issues highlighted by Flake8
Flake8 validates code style against PEP 8 in order to encourage the
writing of idiomatic Python. For reference, I am currently ignoring
errors about line length (E501) because I feel it makes code harder
to read.

This is the invocation I am using:

    $ flake8 --ignore E501 dspace_statistics_api
2018-11-04 00:04:27 +02:00
70dfcb93c5 dspace_statistics_api/database.py: Don't quote host in connect() 2018-11-03 22:43:05 +02:00
69bcd1b5e4 CHANGELOG.md: Add unreleased changes 2018-11-03 22:42:08 +02:00
5f3bd61998 Allow configuration of PostgreSQL port
Defaults to port 5432, but can be overridden with DATABASE_PORT.
2018-11-03 22:40:45 +02:00
e54dd8888f README.md: Update requirements 2018-11-01 16:31:36 +02:00
2ba09f8693 README.md: Improve introduction
Obsessed with the text presentation and line length in GitHub!
2018-11-01 16:28:01 +02:00
a468a87a5a README.md: Improve introduction 2018-11-01 15:58:12 +02:00
6a30b6550d README.md: Improve introduction 2018-11-01 15:45:53 +02:00
18f013bfa0 README.md: Add Falcon to introduction 2018-11-01 10:24:37 +02:00
78900b5d85 CHANGELOG.md: Add changes for v0.6.1 2018-11-01 00:39:12 +02:00
eb08832bf8 Sync API documentation HTML with README.md 2018-11-01 00:37:52 +02:00
c2ec780ad9 README.md: Improve API documentation 2018-11-01 00:37:40 +02:00
df8ebc8bf1 README.md: Improve API endpoint documentation 2018-11-01 00:31:16 +02:00
0d4be5f4c8 README.md: Add API documentation endpoint 2018-11-01 00:22:16 +02:00
30dc7f1939 Add basic API documentation on root (/)
I had imagined plugging in an interactive Swagger or OpenAPI instance
here, but that's actually much more involved in Falcon than I want to
deal with right now.
2018-11-01 00:19:39 +02:00
77194707fd README.md: Improve introduction 2018-11-01 00:08:24 +02:00
10c1f8bdcc README.md: Update Travis CI badge 2018-10-31 23:14:38 +02:00
da74943da2 README.md: Update introduction 2018-10-31 22:40:36 +02:00
fc8348ab29 README.md: Add acknoledgement about the Solr queries 2018-10-31 19:36:50 +02:00
15c3299b99 CHANGELOG.md: Add changes for v0.6.0 2018-10-31 19:26:45 +02:00
d36be5ee50 contrib: Update systemd unit files for refactor 2018-10-28 11:14:21 +02:00
2f45d27554 dspace_statistics_api/app.py: remove unused code
This was added accidentally when I refactored. I was trying to see
if I could use Falcon's on_exit() hook.
2018-10-28 11:14:21 +02:00
b8356f7a87 Add "application" alias to API object
By default gunicorn looks for an "application" object to run, so this
saves us having to type api:app.
2018-10-28 11:14:21 +02:00
2136dc79ce Remove shebang from indexer.py
This is run as a Python module now so does not need a shebang.
2018-10-28 11:14:21 +02:00
ed60120cef Remove executable bit from indexer.py
Now it is run as a Python module.
2018-10-28 11:14:21 +02:00
c027f01b48 Refactor project structure
This follows guidance from several well-known Python best practices
guides. Basically, the idea is create a package for the application
that is comprised of several re-usable modules.

See: https://docs.python-guide.org/writing/structure/
See: https://realpython.com/python-application-layouts/
2018-10-28 11:14:21 +02:00
31 changed files with 3390 additions and 333 deletions

21
.build.yml Normal file
View File

@ -0,0 +1,21 @@
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

153
.drone.yml Normal file
View File

@ -0,0 +1,153 @@
kind: pipeline
type: docker
name: python39
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.9-slim
environment:
PGPASSWORD: dspacestatistics
DATABASE_HOST: database
commands:
- id
- python -V
- apt update && apt install -y gcc
- 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: python38
steps:
- name: database
image: postgres:10-alpine
detach: true
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dspacestatistics
- 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.8-slim
environment:
PGPASSWORD: dspacestatistics
DATABASE_HOST: database
commands:
- id
- python -V
- pip install -r requirements-dev.txt
- pytest
---
kind: pipeline
type: docker
name: python37
steps:
- name: database
image: postgres:10-alpine
detach: true
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dspacestatistics
- 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.7-slim
environment:
PGPASSWORD: dspacestatistics
DATABASE_HOST: database
commands:
- id
- python -V
- pip install -r requirements-dev.txt
- pytest
---
kind: pipeline
type: docker
name: python36
steps:
- name: database
image: postgres:10-alpine
detach: true
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dspacestatistics
- 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.6-slim
environment:
PGPASSWORD: dspacestatistics
DATABASE_HOST: database
commands:
- id
- python -V
- pip install -r requirements-dev.txt
- pytest
# vim: ts=2 sw=2 et

2
.flake8 Normal file
View File

@ -0,0 +1,2 @@
[flake8]
ignore = E501

4
.hound.yml Normal file
View File

@ -0,0 +1,4 @@
flake8:
enabled: true
config_file: .flake8
fail_on_violations: true

View File

@ -1,11 +0,0 @@
language: python
python:
- "3.5"
- "3.6"
- "3.7-dev"
script: pip install -r requirements.txt
branches:
only:
- master
# vim: ts=2 sw=2 et

View File

@ -4,26 +4,143 @@ 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).
### [0.5.2] - 2018-10-28
## Changed
## Unreleased
### Changed
- Add ORDER BY to /items resource to make sure results are returned
deterministically
- Use `fl` parameter in indexer to return only the id field
- Minor refactoring of imports for PEP8 style
## [1.3.2] - 2020-11-18
### Fixed
- Minor issue with limit parameter (> 0)
- Minor issue with limit parameter (<= 100)
### Changed
- Minor refactor in Solr bot filtering
### Updated
- Run poetry update
## [1.3.1] - 2020-10-06
### Changed
- Fix issue with requirements.txt caused by poetry's export
## [1.3.0] - 2020-10-06
### Changed
- Minor refactoring of indexer
### Added
- Ability to get statistics for arbitrary items and date ranges by POSTing a JSON-formatted request to /items as opposed to the current `GET /items` which returns pre-indexed all-time stats for all items
### Updated
- Run pipenv update, bringing minor updates to pytest, psycopg2-binary, etc
## [1.2.1] - 2020-03-02
### Changed
- Help text in API docs should reference UUIDs
- Sample SQL file for tests should use UUIDs
## [1.2.0] - 2020-03-02
### Changed
- Remove Python 3.5 from TravisCI because black requires Python >= 3.6
- Adapt API for DSpace 6+ UUIDs
- This requires dropping the statistics database and re-indexing
### Updated
- Run pipenv update, bringing requests 2.23.0 and pytest 5.3.5
## [1.1.1] - 2019-11-27
### Added
- Configuration for automatic sorting of imports with isort
- Configuration for automatic code formatting with black
### Updated
- Run pipenv update, bringing psycopg2 2.8.4, requests 2.22.0, pytest 5.3.1,
and gunicorn 20.0.4
### Changed
- Use Ubuntu 18.04 "Bionic" for TravisCI builds
- Use Python 3.8.0 for pipenv
- Minor syntax issues highlighted by flake8
## [1.1.0] - 2019-05-05
## Updated
- Falcon 2.0.0 (@alanorth)
## [1.0.0] - 2019-04-15
### Added
- Build configuration for build.sr.ht
### Updated
- Run pipenv update, bringing pytest version 4.4.0, psycopg-binary 2.8.2, etc
- sr.ht and TravisCI configuration to disable emojis and animation to keep logs clean
### Changed
- Use vanilla requests library instead of SolrClient
- Use one-based paging in indexer output (for human readability)
## [0.9.0] - 2019-01-22
### Updated
- pytest version 4.0.0
- Fix indexing of sharded statistics cores ([#10))
- Handle case of missing views/downloads gracefully
## [0.8.1] - 2018-11-14
### Changed
- README.md to recommend using vanilla Python virtual environments and pip instead of pipenv
- Regenerate pipenv environment to capture only direct dependencies
### Added
- `requirements-dev.txt` for installing development packages with pip
## [0.8.0] - 2018-11-11
### Changed
- Properly handle database connection errors
### Added
- API tests with pytest
## [0.7.0] - 2018-11-07
### Added
- Ability to configure PostgreSQL database port with DATABASE_PORT environment variable (defaults to 5432)
- Hound CI configuration to validate pull requests against PEP 8 code style with Flake8
- Configuration for [pipenv](https://pipenv.readthedocs.io/en/latest/)
### Changed
- Use a database management class with Python context management to automatically open/close connections and cursors
### Changed
- Validate code against PEP 8 style guide with Flake8
## [0.6.1] - 2018-10-31
### Added
- API documentation at root path (/)
## [0.6.0] - 2018-10-31
### Changed
- Refactor project structure (note breaking changes to API and indexing invocation, see contrib and README.md)
## [0.5.2] - 2018-10-28
### Changed
- Update library versions in requirements.txt
### [0.5.1] - 2018-10-24
## Changed
## [0.5.1] - 2018-10-24
### Changed
- Use Python's native json instead of ujson
### [0.5.0] - 2018-10-24
## Added
## [0.5.0] - 2018-10-24
### Added
- Example nginx configuration to README.md
## Changed
### Changed
- Don't initialize Solr connection in API
### [0.4.3] - 2018-10-17
## Changed
## [0.4.3] - 2018-10-17
### Changed
- Use pip install as script for Travis CI
## Improved
### Improved
- Documentation for deployment and testing
## [0.4.2] - 2018-10-04
@ -37,7 +154,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.4.1] - 2018-09-26
### Changed
- Use execute_values() to batch insert records to PostgreSQL
- Use `execute_values()` to batch insert records to PostgreSQL
## [0.4.0] - 2018-09-25
### Fixed

View File

@ -1,19 +1,30 @@
# DSpace Statistics API [![Build Status](https://travis-ci.org/alanorth/dspace-statistics-api.svg?branch=master)](https://travis-ci.org/alanorth/dspace-statistics-api)
A simple REST API to expose Solr view and download statistics for items in a DSpace repository. This project contains a standalone indexing component and a WSGI application.
# 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?)
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)
This project contains an indexer and a [Falcon-based](https://falcon.readthedocs.io/) web application to make the 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).
If you use the DSpace Statistics API please cite:
*Orth, A. 2018. DSpace statistics API. Nairobi, Kenya: ILRI. https://hdl.handle.net/10568/99143.*
## Requirements
- Python 3.5+
- Python 3.6+
- PostgreSQL version 9.5+ (due to [`UPSERT` support](https://wiki.postgresql.org/wiki/UPSERT))
- DSpace 4+ with [Solr usage statistics enabled](https://wiki.duraspace.org/display/DSDOC5x/SOLR+Statistics)
- DSpace with [Solr usage statistics enabled](https://wiki.lyrasis.org/display/DSDOC5x/SOLR+Statistics) (tested with 5.8+ and 6.3)
## Installation and Testing
## Installation
Create a Python virtual environment and install the dependencies:
$ python -m venv venv
$ . venv/bin/activate
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install -r requirements.txt
## Running
Set up the environment variables for Solr and PostgreSQL:
$ export SOLR_SERVER=http://localhost:8080/solr
@ -24,16 +35,25 @@ Set up the environment variables for Solr and PostgreSQL:
Index the Solr statistics core to populate the PostgreSQL database:
$ ./indexer.py
$ python -m dspace_statistics_api.indexer
Run the REST API:
$ gunicorn app:api
$ gunicorn dspace_statistics_api.app
Test to see if there are any statistics:
$ curl 'http://localhost:8000/items?limit=1'
## Testing
Install development packages using pip:
$ pip install -r requirements-dev.txt
Run tests:
$ pytest
## Deployment
There are example systemd service and timer units in the `contrib` directory. The API service listens on localhost by default so you will need to expose it publicly using a web server like nginx.
@ -59,21 +79,45 @@ This would expose the API at `/rest/statistics`.
## Using the API
The API exposes the following endpoints:
- GET `/items`return views and downloads for all items that Solr knows about¹. Accepts `limit` and `page` query parameters for pagination of results.
- GET `/item/id`return views and downloads for a single item (*id* must be a positive integer). Returns HTTP 404 if an item id is not found.
- GET `/`return a basic API documentation page.
- GET `/items`return views and downloads for all items that Solr knows about¹. Accepts `limit` and `page` query parameters for pagination of results (`limit` must be an integer between 1 and 100, and `page` must be an integer greater than or equal to 0).
- POST `/items`return views and downloads for an arbitrary list of items with an optional date range. Accepts `limit`, `page`, `dateFrom`, and `dateTo` parameters².
- GET `/item/id`return views and downloads for a single item (`id` must be a UUID). Returns HTTP 404 if an item id is not found.
¹ We are querying the Solr statistics core, which technically only knows about items that have either views or downloads.
The item id is the *internal* UUID for an item. You can get these from the standard DSpace REST API.
## Todo
¹ We are querying the Solr statistics core, which technically only knows about items that have either views or downloads. If an item 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.
² POST requests to `/items` should be in JSON format with the following parameters:
```
{
"limit": 100, // optional, integer between 0 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"
]
}
```
## TODO
- Add API documentation
- Close DB connection when gunicorn shuts down gracefully
- Better logging
- Tests
- Check if database exists (try/except)
- Version API
- Version API (or at least include a /version endpoint?)
- Probably use /status with a version in the response
- Use JSON in PostgreSQL
- Switch to [Python 3.6+ f-string syntax](https://realpython.com/python-f-strings/)
- Add top items endpoint, perhaps `/top/items` or `/items/top`?
- Actually we could add `/items?limit=10&sort=views`
- Make community and collection stats available
- Facet on owningComm and owningColl
- 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).
The license allows you to use and modify the work for personal and commercial purposes, but if you distribute the work you must provide users with a means to access the source code for the version you are distributing. Read more about the [GPLv3 at TL;DR Legal](https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3)).

73
app.py
View File

@ -1,73 +0,0 @@
from database import database_connection
import falcon
db = database_connection()
db.set_session(readonly=True)
class AllItemsResource:
def on_get(self, req, resp):
"""Handles GET requests"""
# Return HTTPBadRequest if id parameter is not present and valid
limit = req.get_param_as_int("limit", min=0, max=100) or 100
page = req.get_param_as_int("page", min=0) or 0
offset = limit * page
cursor = db.cursor()
# get total number of items so we can estimate the pages
cursor.execute('SELECT COUNT(id) FROM items')
pages = round(cursor.fetchone()[0] / limit)
# get statistics, ordered by id, and use limit and offset to page through results
cursor.execute('SELECT id, views, downloads FROM items ORDER BY id ASC LIMIT {} OFFSET {}'.format(limit, offset))
# create a list to hold dicts of item stats
statistics = list()
# iterate over results and build statistics object
for item in cursor:
statistics.append({ 'id': item['id'], 'views': item['views'], 'downloads': item['downloads'] })
cursor.close()
message = {
'currentPage': page,
'totalPages': pages,
'limit': limit,
'statistics': statistics
}
resp.media = message
class ItemResource:
def on_get(self, req, resp, item_id):
"""Handles GET requests"""
cursor = db.cursor()
cursor.execute('SELECT views, downloads FROM items WHERE id={}'.format(item_id))
if cursor.rowcount == 0:
raise falcon.HTTPNotFound(
title='Item not found',
description='The item with id "{}" was not found.'.format(item_id)
)
else:
results = cursor.fetchone()
statistics = {
'id': item_id,
'views': results['views'],
'downloads': results['downloads']
}
resp.media = statistics
cursor.close()
def on_exit(api):
print("Shutting down DB")
api = falcon.API()
api.add_route('/items', AllItemsResource())
api.add_route('/item/{item_id:int}', ItemResource())
# vim: set sw=4 ts=4 expandtab:

View File

@ -1,11 +0,0 @@
import os
# Check if Solr connection information was provided in the environment
SOLR_SERVER = os.environ.get('SOLR_SERVER', 'http://localhost:8080/solr')
DATABASE_NAME = os.environ.get('DATABASE_NAME', 'dspacestatistics')
DATABASE_USER = os.environ.get('DATABASE_USER', 'dspacestatistics')
DATABASE_PASS = os.environ.get('DATABASE_PASS', 'dspacestatistics')
DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost')
# vim: set sw=4 ts=4 expandtab:

View File

@ -12,7 +12,7 @@ Group=nogroup
WorkingDirectory=/var/lib/dspace-statistics-api
ExecStart=/var/lib/dspace-statistics-api/venv/bin/gunicorn \
--bind 127.0.0.1:5000 \
app:api
dspace_statistics_api.app
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID

View File

@ -11,7 +11,7 @@ Environment=DATABASE_HOST=localhost
User=nobody
Group=nogroup
WorkingDirectory=/var/lib/dspace-statistics-api
ExecStart=/var/lib/dspace-statistics-api/venv/bin/python indexer.py
ExecStart=/var/lib/dspace-statistics-api/venv/bin/python -m dspace_statistics_api.indexer
[Install]
WantedBy=multi-user.target

View File

@ -1,12 +0,0 @@
from config import DATABASE_NAME
from config import DATABASE_USER
from config import DATABASE_PASS
from config import DATABASE_HOST
import psycopg2, psycopg2.extras
def database_connection():
connection = psycopg2.connect("dbname={} user={} password={} host='{}'".format(DATABASE_NAME, DATABASE_USER, DATABASE_PASS, DATABASE_HOST), cursor_factory=psycopg2.extras.DictCursor)
return connection
# vim: set sw=4 ts=4 expandtab:

View File

View File

@ -0,0 +1,158 @@
import falcon
import psycopg2.extras
from .database import DatabaseManager
from .items import get_downloads, get_views
from .util import validate_items_post_parameters
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()
class AllItemsResource:
def on_get(self, req, resp):
"""Handles GET requests"""
# Return HTTPBadRequest if id parameter is not present and valid
limit = req.get_param_as_int("limit", min_value=1, max_value=100) or 100
page = req.get_param_as_int("page", min_value=0) or 0
offset = limit * page
with DatabaseManager() as db:
db.set_session(readonly=True)
with db.cursor() as cursor:
# get total number of items so we can estimate the pages
cursor.execute("SELECT COUNT(id) FROM items")
pages = round(cursor.fetchone()[0] / limit)
# get statistics and use limit and offset to page through results
cursor.execute(
"SELECT id, views, downloads FROM items ORDER BY id LIMIT %s OFFSET %s",
[limit, offset],
)
# create a list to hold dicts of item stats
statistics = list()
# iterate over results and build statistics object
for item in cursor:
statistics.append(
{
"id": str(item["id"]),
"views": item["views"],
"downloads": item["downloads"],
}
)
message = {
"currentPage": page,
"totalPages": pages,
"limit": limit,
"statistics": statistics,
}
resp.media = message
@falcon.before(validate_items_post_parameters)
def on_post(self, req, resp):
"""Handles POST requests"""
# Build the Solr date string, ie: [* TO *]
if req.context.dateFrom and req.context.dateTo:
solr_date_string = f"[{req.context.dateFrom} TO {req.context.dateTo}]"
elif not req.context.dateFrom and req.context.dateTo:
solr_date_string = f"[* TO {req.context.dateTo}]"
elif req.context.dateFrom and not req.context.dateTo:
solr_date_string = f"[{req.context.dateFrom} TO *]"
else:
solr_date_string = "[* TO *]"
# Helper variables to make working with pages/items/results easier and
# to make the code easier to understand
number_of_items: int = len(req.context.items)
pages: int = int(number_of_items / req.context.limit)
first_item: int = req.context.page * req.context.limit
last_item: int = first_item + req.context.limit
# Get a subset of the POSTed items based on our limit. Note that Python
# list slicing and indexing are both zero based, but the first and last
# items in a slice can be confusing. See this ASCII diagram:
#
# +---+---+---+---+---+---+
# | P | y | t | h | o | n |
# +---+---+---+---+---+---+
# Slice position: 0 1 2 3 4 5 6
# Index position: 0 1 2 3 4 5
#
# So if we have a list items with 240 items:
#
# 1st set: items[0:100] would give items at indexes 0 to 99
# 2nd set: items[100:200] would give items at indexes 100 to 199
# 3rd set: items[200:300] would give items at indexes 200 to 239
items_subset: list = req.context.items[first_item:last_item]
views: dict = get_views(solr_date_string, items_subset)
downloads: dict = get_downloads(solr_date_string, items_subset)
# create a list to hold dicts of item stats
statistics = list()
# iterate over views dict to extract views and use the item id as an
# index to the downloads dict to extract downloads.
for k, v in views.items():
statistics.append({"id": k, "views": v, "downloads": downloads[k]})
message = {
"currentPage": req.context.page,
"totalPages": pages,
"limit": req.context.limit,
"statistics": statistics,
}
resp.status = falcon.HTTP_200
resp.media = message
class ItemResource:
def on_get(self, req, resp, item_id):
"""Handles GET requests"""
# Adapt Pythons uuid.UUID type to PostgreSQLs uuid
# See: https://www.psycopg.org/docs/extras.html
psycopg2.extras.register_uuid()
with DatabaseManager() as db:
db.set_session(readonly=True)
with db.cursor() as cursor:
cursor = db.cursor()
cursor.execute(
"SELECT views, downloads FROM items WHERE id=%s", [str(item_id)]
)
if cursor.rowcount == 0:
raise falcon.HTTPNotFound(
title="Item not found",
description=f'The item with id "{str(item_id)}" was not found.',
)
else:
results = cursor.fetchone()
statistics = {
"id": str(item_id),
"views": results["views"],
"downloads": results["downloads"],
}
resp.media = statistics
api = application = falcon.API()
api.add_route("/", RootResource())
api.add_route("/items", AllItemsResource())
api.add_route("/item/{item_id:uuid}", ItemResource())
# vim: set sw=4 ts=4 expandtab:

View File

@ -0,0 +1,12 @@
import os
# Check if Solr connection information was provided in the environment
SOLR_SERVER = os.environ.get("SOLR_SERVER", "http://localhost:8080/solr")
DATABASE_NAME = os.environ.get("DATABASE_NAME", "dspacestatistics")
DATABASE_USER = os.environ.get("DATABASE_USER", "dspacestatistics")
DATABASE_PASS = os.environ.get("DATABASE_PASS", "dspacestatistics")
DATABASE_HOST = os.environ.get("DATABASE_HOST", "localhost")
DATABASE_PORT = os.environ.get("DATABASE_PORT", "5432")
# vim: set sw=4 ts=4 expandtab:

View File

@ -0,0 +1,36 @@
import falcon
import psycopg2
import psycopg2.extras
from .config import (
DATABASE_HOST,
DATABASE_NAME,
DATABASE_PASS,
DATABASE_PORT,
DATABASE_USER,
)
class DatabaseManager:
"""Manage database connection."""
def __init__(self):
self._connection_uri = f"dbname={DATABASE_NAME} user={DATABASE_USER} password={DATABASE_PASS} host={DATABASE_HOST} port={DATABASE_PORT}"
def __enter__(self):
try:
self._connection = psycopg2.connect(
self._connection_uri, cursor_factory=psycopg2.extras.DictCursor
)
except psycopg2.OperationalError:
title = "500 Internal Server Error"
description = "Could not connect to database"
raise falcon.HTTPInternalServerError(title, description)
return self._connection
def __exit__(self, exc_type, exc_value, exc_traceback):
self._connection.close()
# vim: set sw=4 ts=4 expandtab:

View File

@ -0,0 +1,37 @@
<!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>
</ul>
<p>The item id is the <em>internal</em> uuid for an item. 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 that have either views or downloads. If an item 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> should be in JSON format with the following parameters:
<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,227 @@
#
# 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 item views and downloads
# into a PostgreSQL database for use by other applications (like an API).
#
# This script is written for Python 3.6+ and requires several modules that you
# can install with pip (I recommend using a Python virtual environment):
#
# $ pip install psycopg2-binary
#
# See: https://wiki.duraspace.org/display/DSPACE/Solr
import psycopg2.extras
import requests
from .config import SOLR_SERVER
from .database import DatabaseManager
from .util import get_statistics_shards
def index_views():
# get total number of distinct facets for items with a minimum of 1 view,
# otherwise Solr returns all kinds of weird ids that are actually not in
# the database. Also, stats are expensive, but we need stats.calcdistinct
# so we can get the countDistinct summary to calculate how many pages of
# results we have.
#
# see: https://lucene.apache.org/solr/guide/6_6/the-stats-component.html
solr_query_params = {
"q": "type:2",
"fq": "-isBot:true AND statistics_type:view",
"fl": "id",
"facet": "true",
"facet.field": "id",
"facet.mincount": 1,
"facet.limit": 1,
"facet.offset": 0,
"stats": "true",
"stats.field": "id",
"stats.calcdistinct": "true",
"shards": shards,
"rows": 0,
"wt": "json",
}
solr_url = SOLR_SERVER + "/statistics/select"
res = requests.get(solr_url, params=solr_query_params)
try:
# get total number of distinct facets (countDistinct)
results_totalNumFacets = res.json()["stats"]["stats_fields"]["id"][
"countDistinct"
]
except TypeError:
print("No item views to index, exiting.")
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_current_page = 0
with DatabaseManager() as db:
with db.cursor() as cursor:
# create an empty list to store values for batch insertion
data = []
while results_current_page <= results_num_pages:
# "pages" are zero based, but one based is more human readable
print(
f"Indexing item views (page {results_current_page + 1} of {results_num_pages + 1})"
)
solr_query_params = {
"q": "type:2",
"fq": "-isBot:true AND statistics_type:view",
"fl": "id",
"facet": "true",
"facet.field": "id",
"facet.mincount": 1,
"facet.limit": results_per_page,
"facet.offset": results_current_page * results_per_page,
"shards": shards,
"rows": 0,
"wt": "json",
"json.nl": "map", # return facets as a dict instead of a flat list
}
res = requests.get(solr_url, params=solr_query_params)
# Solr returns facets as a dict of dicts (see json.nl parameter)
views = res.json()["facet_counts"]["facet_fields"]
# iterate over the 'id' dict and get the item ids and views
for item_id, item_views in views["id"].items():
data.append((item_id, item_views))
# do a batch insert of values from the current "page" of results
sql = "INSERT INTO items(id, views) VALUES %s ON CONFLICT(id) DO UPDATE SET views=excluded.views"
psycopg2.extras.execute_values(cursor, sql, data, template="(%s, %s)")
db.commit()
# clear all items from the list so we can populate it with the next batch
data.clear()
results_current_page += 1
def index_downloads():
# get the total number of distinct facets for items with at least 1 download
solr_query_params = {
"q": "type:0",
"fq": "-isBot:true AND statistics_type:view AND bundleName:ORIGINAL",
"fl": "owningItem",
"facet": "true",
"facet.field": "owningItem",
"facet.mincount": 1,
"facet.limit": 1,
"facet.offset": 0,
"stats": "true",
"stats.field": "owningItem",
"stats.calcdistinct": "true",
"shards": shards,
"rows": 0,
"wt": "json",
}
solr_url = SOLR_SERVER + "/statistics/select"
res = requests.get(solr_url, params=solr_query_params)
try:
# get total number of distinct facets (countDistinct)
results_totalNumFacets = res.json()["stats"]["stats_fields"]["owningItem"][
"countDistinct"
]
except TypeError:
print("No item downloads to index, exiting.")
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_current_page = 0
with DatabaseManager() as db:
with db.cursor() as cursor:
# create an empty list to store values for batch insertion
data = []
while results_current_page <= results_num_pages:
# "pages" are zero based, but one based is more human readable
print(
f"Indexing item downloads (page {results_current_page + 1} of {results_num_pages + 1})"
)
solr_query_params = {
"q": "type:0",
"fq": "-isBot:true AND statistics_type:view AND bundleName:ORIGINAL",
"fl": "owningItem",
"facet": "true",
"facet.field": "owningItem",
"facet.mincount": 1,
"facet.limit": results_per_page,
"facet.offset": results_current_page * results_per_page,
"shards": shards,
"rows": 0,
"wt": "json",
"json.nl": "map", # return facets as a dict instead of a flat list
}
res = requests.get(solr_url, params=solr_query_params)
# Solr returns facets as a dict of dicts (see json.nl parameter)
downloads = res.json()["facet_counts"]["facet_fields"]
# iterate over the 'owningItem' dict and get the item ids and downloads
for item_id, item_downloads in downloads["owningItem"].items():
data.append((item_id, item_downloads))
# do a batch insert of values from the current "page" of results
sql = "INSERT INTO items(id, downloads) VALUES %s ON CONFLICT(id) DO UPDATE SET downloads=excluded.downloads"
psycopg2.extras.execute_values(cursor, sql, data, template="(%s, %s)")
db.commit()
# clear all items from the list so we can populate it with the next batch
data.clear()
results_current_page += 1
with DatabaseManager() as db:
with db.cursor() as cursor:
# create table to store item views and downloads
cursor.execute(
"""CREATE TABLE IF NOT EXISTS items
(id UUID PRIMARY KEY, views INT DEFAULT 0, downloads INT DEFAULT 0)"""
)
# commit the table creation before closing the database connection
db.commit()
shards = get_statistics_shards()
index_views()
index_downloads()
# vim: set sw=4 ts=4 expandtab:

View File

@ -0,0 +1,107 @@
import requests
from .config import SOLR_SERVER
from .util import get_statistics_shards
def get_views(solr_date_string: str, items: list):
"""
Get view statistics for a list of items from Solr.
:parameter solr_date_string (str): Solr date string, for example "[* TO *]"
:parameter items (list): a list of item IDs
:returns: A dict of item IDs and views
"""
shards = get_statistics_shards()
# Join the UUIDs with "OR" and escape the hyphens for Solr
solr_items_string: str = " OR ".join(items).replace("-", r"\-")
solr_query_params = {
"q": f"id:({solr_items_string})",
"fq": f"type:2 AND isBot:false AND statistics_type:view AND time:{solr_date_string}",
"fl": "id",
"facet": "true",
"facet.field": "id",
"facet.mincount": 1,
"shards": shards,
"rows": 0,
"wt": "json",
"json.nl": "map", # return facets as a dict instead of a flat list
}
solr_url = SOLR_SERVER + "/statistics/select"
res = requests.get(solr_url, params=solr_query_params)
# Create an empty dict to store views
data = {}
# Solr returns facets as a dict of dicts (see the json.nl parameter)
views = res.json()["facet_counts"]["facet_fields"]
# iterate over the 'id' dict and get the item ids and views
for item_id, item_views in views["id"].items():
data[item_id] = item_views
# Check if any items have missing stats so we can set them to 0
if len(data) < len(items):
# List comprehension to get a list of item ids (keys) in the data
data_ids = [k for k, v in data.items()]
for item_id in items:
if item_id not in data_ids:
data[item_id] = 0
continue
return data
def get_downloads(solr_date_string: str, items: list):
"""
Get download statistics for a list of items from Solr.
:parameter solr_date_string (str): Solr date string, for example "[* TO *]"
:parameter items (list): a list of item IDs
:returns: A dict of item IDs and downloads
"""
shards = get_statistics_shards()
# Join the UUIDs with "OR" and escape the hyphens for Solr
solr_items_string: str = " OR ".join(items).replace("-", r"\-")
solr_query_params = {
"q": f"owningItem:({solr_items_string})",
"fq": f"type:0 AND isBot:false AND statistics_type:view AND bundleName:ORIGINAL AND time:{solr_date_string}",
"fl": "owningItem",
"facet": "true",
"facet.field": "owningItem",
"facet.mincount": 1,
"shards": shards,
"rows": 0,
"wt": "json",
"json.nl": "map", # return facets as a dict instead of a flat list
}
solr_url = SOLR_SERVER + "/statistics/select"
res = requests.get(solr_url, params=solr_query_params)
# Create an empty dict to store downloads
data = {}
# Solr returns facets as a dict of dicts (see the json.nl parameter)
downloads = res.json()["facet_counts"]["facet_fields"]
# Iterate over the 'owningItem' dict and get the item ids and downloads
for item_id, item_downloads in downloads["owningItem"].items():
data[item_id] = item_downloads
# Check if any items have missing stats so we can set them to 0
if len(data) < len(items):
# List comprehension to get a list of item ids (keys) in the data
data_ids = [k for k, v in data.items()]
for item_id in items:
if item_id not in data_ids:
data[item_id] = 0
continue
return data
# vim: set sw=4 ts=4 expandtab:

View File

@ -0,0 +1,138 @@
import datetime
import json
import re
import falcon
import requests
from .config import SOLR_SERVER
def get_statistics_shards():
"""Enumerate the cores in Solr to determine if statistics have been sharded into
yearly shards by DSpace's stats-util or not (for example: statistics-2018).
Returns:
str:A list of Solr statistics shards separated by commas.
"""
# Initialize an empty list for statistics core years
statistics_core_years = []
# URL for Solr status to check active cores
solr_query_params = {"action": "STATUS", "wt": "json"}
solr_url = SOLR_SERVER + "/admin/cores"
res = requests.get(solr_url, params=solr_query_params)
if res.status_code == requests.codes.ok:
data = res.json()
# Iterate over active cores from Solr's STATUS response (cores are in
# the status array of this response).
for core in data["status"]:
# Pattern to match, for example: statistics-2018
pattern = re.compile("^statistics-[0-9]{4}$")
if not pattern.match(core):
continue
# Append current core to list
statistics_core_years.append(core)
# Initialize a string to hold our shards (may end up being empty if the Solr
# core has not been processed by stats-util).
shards = str()
if len(statistics_core_years) > 0:
# Begin building a string of shards starting with the default one
shards = f"{SOLR_SERVER}/statistics"
for core in statistics_core_years:
# Create a comma-separated list of shards to pass to our Solr query
#
# See: https://wiki.apache.org/solr/DistributedSearch
shards += f",{SOLR_SERVER}/{core}"
# Return the string of shards, which may actually be empty. Solr doesn't
# seem to mind if the shards query parameter is empty and I haven't seen
# any negative performance impact so this should be fine.
return shards
def is_valid_date(date):
try:
# Solr date format is: 2020-01-01T00:00:00Z
# See: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
return True
except ValueError:
raise falcon.HTTPBadRequest(
title="Invalid parameter",
description=f"Invalid date format: {date}. The value must be in format: 2020-01-01T00:00:00Z.",
)
def validate_items_post_parameters(req, resp, resource, params):
"""Check the POSTed request parameters for the `/items` endpoint.
Meant to be used as a `before` hook.
"""
# Only attempt to read the POSTed request if its length is not 0 (or
# rather, in the Python sense, if length is not a False-y value).
if req.content_length:
doc = json.load(req.bounded_stream)
else:
raise falcon.HTTPBadRequest(
title="Invalid request", description="Request body is empty."
)
# Parse date parameters from request body (will raise an HTTPBadRequest
# from is_valid_date() if any parameters are invalid)
if "dateFrom" in doc and is_valid_date(doc["dateFrom"]):
req.context.dateFrom = doc["dateFrom"]
else:
req.context.dateFrom = None
if "dateTo" in doc and is_valid_date(doc["dateTo"]):
req.context.dateTo = doc["dateTo"]
else:
req.context.dateTo = None
# Parse the limit parameter from the POST request body
if "limit" in doc:
if isinstance(doc["limit"], int) and 0 < doc["limit"] <= 100:
req.context.limit = doc["limit"]
else:
raise falcon.HTTPBadRequest(
title="Invalid parameter",
description='The "limit" parameter is invalid. The value must be an integer between 1 and 100.',
)
else:
req.context.limit = 100
# Parse the page parameter from the POST request body
if "page" in doc:
if isinstance(doc["page"], int) and doc["page"] >= 0:
req.context.page = doc["page"]
else:
raise falcon.HTTPBadRequest(
title="Invalid parameter",
description='The "page" parameter is invalid. The value must be at least 0.',
)
else:
req.context.page = 0
# Parse the list of items from the POST request body
if "items" in doc:
if isinstance(doc["items"], list) and len(doc["items"]) > 0:
req.context.items = doc["items"]
else:
raise falcon.HTTPBadRequest(
title="Invalid parameter",
description='The "items" parameter is invalid. The value must be a comma-separated list of item UUIDs.',
)
else:
req.context.items = list()

View File

@ -1,173 +0,0 @@
#!/usr/bin/env python
#
# 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 item views and downloads
# into a PostgreSQL database for use by other applications (like an API).
#
# This script is written for Python 3.5+ and requires several modules that you
# can install with pip (I recommend using a Python virtual environment):
#
# $ pip install SolrClient psycopg2-binary
#
# See: https://solrclient.readthedocs.io/en/latest/SolrClient.html
# See: https://wiki.duraspace.org/display/DSPACE/Solr
from database import database_connection
import json
import psycopg2.extras
from solr import solr_connection
def index_views():
# get total number of distinct facets for items with a minimum of 1 view,
# otherwise Solr returns all kinds of weird ids that are actually not in
# the database. Also, stats are expensive, but we need stats.calcdistinct
# so we can get the countDistinct summary.
#
# see: https://lucene.apache.org/solr/guide/6_6/the-stats-component.html
res = solr.query('statistics', {
'q':'type:2',
'fq':'isBot:false AND statistics_type:view',
'facet':True,
'facet.field':'id',
'facet.mincount':1,
'facet.limit':1,
'facet.offset':0,
'stats':True,
'stats.field':'id',
'stats.calcdistinct':True
}, rows=0)
# get total number of distinct facets (countDistinct)
results_totalNumFacets = json.loads(res.get_json())['stats']['stats_fields']['id']['countDistinct']
# 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_current_page = 0
cursor = db.cursor()
# create an empty list to store values for batch insertion
data = []
while results_current_page <= results_num_pages:
print('Indexing item views (page {} of {})'.format(results_current_page, results_num_pages))
res = solr.query('statistics', {
'q':'type:2',
'fq':'isBot:false AND statistics_type:view',
'facet':True,
'facet.field':'id',
'facet.mincount':1,
'facet.limit':results_per_page,
'facet.offset':results_current_page * results_per_page
}, rows=0)
# SolrClient's get_facets() returns a dict of dicts
views = res.get_facets()
# in this case iterate over the 'id' dict and get the item ids and views
for item_id, item_views in views['id'].items():
data.append((item_id, item_views))
# do a batch insert of values from the current "page" of results
sql = 'INSERT INTO items(id, views) VALUES %s ON CONFLICT(id) DO UPDATE SET views=excluded.views'
psycopg2.extras.execute_values(cursor, sql, data, template='(%s, %s)')
db.commit()
# clear all items from the list so we can populate it with the next batch
data.clear()
results_current_page += 1
cursor.close()
def index_downloads():
# get the total number of distinct facets for items with at least 1 download
res = solr.query('statistics', {
'q':'type:0',
'fq':'isBot:false AND statistics_type:view AND bundleName:ORIGINAL',
'facet':True,
'facet.field':'owningItem',
'facet.mincount':1,
'facet.limit':1,
'facet.offset':0,
'stats':True,
'stats.field':'owningItem',
'stats.calcdistinct':True
}, rows=0)
# get total number of distinct facets (countDistinct)
results_totalNumFacets = json.loads(res.get_json())['stats']['stats_fields']['owningItem']['countDistinct']
# 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_current_page = 0
cursor = db.cursor()
# create an empty list to store values for batch insertion
data = []
while results_current_page <= results_num_pages:
print('Indexing item downloads (page {} of {})'.format(results_current_page, results_num_pages))
res = solr.query('statistics', {
'q':'type:0',
'fq':'isBot:false AND statistics_type:view AND bundleName:ORIGINAL',
'facet':True,
'facet.field':'owningItem',
'facet.mincount':1,
'facet.limit':results_per_page,
'facet.offset':results_current_page * results_per_page
}, rows=0)
# SolrClient's get_facets() returns a dict of dicts
downloads = res.get_facets()
# in this case iterate over the 'owningItem' dict and get the item ids and downloads
for item_id, item_downloads in downloads['owningItem'].items():
data.append((item_id, item_downloads))
# do a batch insert of values from the current "page" of results
sql = 'INSERT INTO items(id, downloads) VALUES %s ON CONFLICT(id) DO UPDATE SET downloads=excluded.downloads'
psycopg2.extras.execute_values(cursor, sql, data, template='(%s, %s)')
db.commit()
# clear all items from the list so we can populate it with the next batch
data.clear()
results_current_page += 1
cursor.close()
db = database_connection()
solr = solr_connection()
# create table to store item views and downloads
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS items
(id INT PRIMARY KEY, views INT DEFAULT 0, downloads INT DEFAULT 0)''')
index_views()
index_downloads()
db.close()
# vim: set sw=4 ts=4 expandtab:

852
poetry.lock generated Normal file
View File

@ -0,0 +1,852 @@
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "appnope"
version = "0.1.2"
description = "Disable App Nap on macOS >= 10.9"
category = "dev"
optional = false
python-versions = "*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform == \"darwin\""
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
marker = "sys_platform == \"win32\""
[[package]]
name = "attrs"
version = "20.3.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface"]
tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
[[package]]
name = "backcall"
version = "0.2.0"
description = "Specifications for callback functions passed in to an API"
category = "dev"
optional = false
python-versions = "*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[[package]]
name = "black"
version = "20.8b1"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[package.dependencies]
appdirs = "*"
click = ">=7.1.2"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.6,<1"
regex = ">=2020.1.8"
toml = ">=0.10.1"
typed-ast = ">=1.4.0"
typing-extensions = ">=3.7.4"
[package.dependencies.dataclasses]
version = ">=0.6"
python = "<3.7"
[[package]]
name = "certifi"
version = "2020.12.5"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "chardet"
version = "4.0.0"
description = "Universal encoding detector for Python 2 and 3"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform == \"win32\" or sys_platform == \"win32\""
[[package]]
name = "dataclasses"
version = "0.6"
description = "A backport of the dataclasses module for Python 3.6"
category = "dev"
optional = false
python-versions = "*"
marker = "python_version < \"3.7\""
[[package]]
name = "decorator"
version = "4.4.2"
description = "Decorators for Humans"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[[package]]
name = "falcon"
version = "2.0.0"
description = "An unladen web framework for building APIs and app backends."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "flake8"
version = "3.8.4"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.6.0a1,<2.7.0"
pyflakes = ">=2.2.0,<2.3.0"
[package.dependencies.importlib-metadata]
version = "*"
python = "<3.8"
[[package]]
name = "gunicorn"
version = "20.0.4"
description = "WSGI HTTP Server for UNIX"
category = "main"
optional = false
python-versions = ">=3.4"
[package.extras]
eventlet = ["eventlet (>=0.9.7)"]
gevent = ["gevent (>=0.13)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]
[package.dependencies]
setuptools = ">=3.0"
[[package]]
name = "idna"
version = "2.10"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "importlib-metadata"
version = "3.3.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
marker = "python_version < \"3.8\""
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[package.dependencies]
zipp = ">=0.5"
[package.dependencies.typing-extensions]
version = ">=3.6.4"
python = "<3.8"
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "ipython"
version = "7.19.0"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
python-versions = ">=3.7"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[package.extras]
all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"]
doc = ["Sphinx (>=1.3)"]
kernel = ["ipykernel"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["notebook", "ipywidgets"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"]
[package.dependencies]
appnope = "*"
backcall = "*"
colorama = "*"
decorator = "*"
jedi = ">=0.10"
pexpect = ">4.3"
pickleshare = "*"
prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
pygments = "*"
setuptools = ">=18.5"
traitlets = ">=4.2"
[[package]]
name = "ipython-genutils"
version = "0.2.0"
description = "Vestigial utilities from IPython"
category = "dev"
optional = false
python-versions = "*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[[package]]
name = "isort"
version = "5.6.4"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
[package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
[[package]]
name = "jedi"
version = "0.17.2"
description = "An autocompletion tool for Python that can be used for text editors."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[package.extras]
qa = ["flake8 (3.7.9)"]
testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"]
[package.dependencies]
parso = ">=0.7.0,<0.8.0"
[[package]]
name = "mccabe"
version = "0.6.1"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "20.8"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pyparsing = ">=2.0.2"
[[package]]
name = "parso"
version = "0.7.1"
description = "A Python Parser"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[package.extras]
testing = ["docopt", "pytest (>=3.0.7)"]
[[package]]
name = "pathspec"
version = "0.8.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pexpect"
version = "4.8.0"
description = "Pexpect allows easy control of interactive console applications."
category = "dev"
optional = false
python-versions = "*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform != \"win32\""
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "pickleshare"
version = "0.7.5"
description = "Tiny 'shelve'-like database with concurrency support"
category = "dev"
optional = false
python-versions = "*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[[package]]
name = "pluggy"
version = "0.13.1"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
dev = ["pre-commit", "tox"]
[package.dependencies]
[package.dependencies.importlib-metadata]
version = ">=0.12"
python = "<3.8"
[[package]]
name = "prompt-toolkit"
version = "3.0.8"
description = "Library for building powerful interactive command lines in Python"
category = "dev"
optional = false
python-versions = ">=3.6.1"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[package.dependencies]
wcwidth = "*"
[[package]]
name = "psycopg2-binary"
version = "2.8.6"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[[package]]
name = "ptyprocess"
version = "0.6.0"
description = "Run a subprocess in a pseudo terminal"
category = "dev"
optional = false
python-versions = "*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform != \"win32\""
[[package]]
name = "py"
version = "1.10.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pycodestyle"
version = "2.6.0"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyflakes"
version = "2.2.0"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pygments"
version = "2.7.3"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
python-versions = ">=3.5"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[[package]]
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "pytest"
version = "6.2.1"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[package.dependencies]
atomicwrites = ">=1.0"
attrs = ">=19.2.0"
colorama = "*"
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0.0a1"
py = ">=1.8.2"
toml = "*"
[package.dependencies.importlib-metadata]
version = ">=0.12"
python = "<3.8"
[[package]]
name = "regex"
version = "2020.11.13"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "requests"
version = "2.25.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<5"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.27"
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "traitlets"
version = "5.0.5"
description = "Traitlets Python configuration system"
category = "dev"
optional = false
python-versions = ">=3.7"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[package.extras]
test = ["pytest"]
[package.dependencies]
ipython-genutils = "*"
[[package]]
name = "typed-ast"
version = "1.4.1"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "typing-extensions"
version = "3.7.4.3"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[[package]]
name = "wcwidth"
version = "0.2.5"
description = "Measures the displayed width of unicode strings in a terminal"
category = "dev"
optional = false
python-versions = "*"
marker = "python_version >= \"3.7\" and python_version < \"4.0\""
[[package]]
name = "zipp"
version = "3.4.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.6"
marker = "python_version < \"3.8\""
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
[metadata]
lock-version = "1.0"
python-versions = "^3.6"
content-hash = "3cd45aacbfab0e85f74c7a010443432f4d6bf0f6cd3bbb84e11c8c7ea20a4613"
[metadata.files]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
appnope = [
{file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
{file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
]
backcall = [
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
dataclasses = [
{file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"},
{file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"},
]
decorator = [
{file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"},
{file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"},
]
falcon = [
{file = "falcon-2.0.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983"},
{file = "falcon-2.0.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f93351459f110b4c1ee28556aef9a791832df6f910bea7b3f616109d534df06b"},
{file = "falcon-2.0.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e9efa0791b5d9f9dd9689015ea6bce0a27fcd5ecbcd30e6d940bffa4f7f03389"},
{file = "falcon-2.0.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:59d1e8c993b9a37ea06df9d72cf907a46cc8063b30717cdac2f34d1658b6f936"},
{file = "falcon-2.0.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:a5ebb22a04c9cc65081938ee7651b4e3b4d2a28522ea8ec04c7bdd2b3e9e8cd8"},
{file = "falcon-2.0.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:95bf6ce986c1119aef12c9b348f4dee9c6dcc58391bdd0bc2b0bf353c2b15986"},
{file = "falcon-2.0.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:aa184895d1ad4573fbfaaf803563d02f019ebdf4790e41cc568a330607eae439"},
{file = "falcon-2.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:74cf1d18207381c665b9e6292d65100ce146d958707793174b03869dc6e614f4"},
{file = "falcon-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24adcd2b29a8ffa9d552dc79638cd21736a3fb04eda7d102c6cebafdaadb88ad"},
{file = "falcon-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:18157af2a4fc3feedf2b5dcc6196f448639acf01c68bc33d4d5a04c3ef87f494"},
{file = "falcon-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e3782b7b92fefd46a6ad1fd8fe63fe6c6f1b7740a95ca56957f48d1aee34b357"},
{file = "falcon-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:9712975adcf8c6e12876239085ad757b8fdeba223d46d23daef82b47658f83a9"},
{file = "falcon-2.0.0-py2.py3-none-any.whl", hash = "sha256:54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53"},
{file = "falcon-2.0.0.tar.gz", hash = "sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc"},
]
flake8 = [
{file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
{file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"},
]
gunicorn = [
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
{file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
importlib-metadata = [
{file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"},
{file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
ipython = [
{file = "ipython-7.19.0-py3-none-any.whl", hash = "sha256:c987e8178ced651532b3b1ff9965925bfd445c279239697052561a9ab806d28f"},
{file = "ipython-7.19.0.tar.gz", hash = "sha256:cbb2ef3d5961d44e6a963b9817d4ea4e1fa2eb589c371a470fed14d8d40cbd6a"},
]
ipython-genutils = [
{file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
{file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"},
]
isort = [
{file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"},
{file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"},
]
jedi = [
{file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"},
{file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
{file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"},
]
parso = [
{file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"},
{file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"},
]
pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
]
pexpect = [
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
]
pickleshare = [
{file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
prompt-toolkit = [
{file = "prompt_toolkit-3.0.8-py3-none-any.whl", hash = "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"},
{file = "prompt_toolkit-3.0.8.tar.gz", hash = "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c"},
]
psycopg2-binary = [
{file = "psycopg2-binary-2.8.6.tar.gz", hash = "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0"},
{file = "psycopg2_binary-2.8.6-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4"},
{file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db"},
{file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"},
{file = "psycopg2_binary-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25"},
{file = "psycopg2_binary-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c"},
{file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c"},
{file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1"},
{file = "psycopg2_binary-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2"},
{file = "psycopg2_binary-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152"},
{file = "psycopg2_binary-2.8.6-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449"},
{file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859"},
{file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550"},
{file = "psycopg2_binary-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd"},
{file = "psycopg2_binary-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71"},
{file = "psycopg2_binary-2.8.6-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4"},
{file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb"},
{file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da"},
{file = "psycopg2_binary-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2"},
{file = "psycopg2_binary-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a"},
{file = "psycopg2_binary-2.8.6-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"},
{file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf"},
{file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b"},
{file = "psycopg2_binary-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67"},
{file = "psycopg2_binary-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66"},
{file = "psycopg2_binary-2.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f"},
{file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77"},
{file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94"},
{file = "psycopg2_binary-2.8.6-cp38-cp38-win32.whl", hash = "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729"},
{file = "psycopg2_binary-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77"},
{file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52"},
{file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd"},
]
ptyprocess = [
{file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"},
{file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"},
]
py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
]
pycodestyle = [
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
]
pyflakes = [
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
]
pygments = [
{file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"},
{file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytest = [
{file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"},
{file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"},
]
regex = [
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
{file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
{file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
{file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
{file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
{file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
{file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
{file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
{file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
{file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
{file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
{file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
{file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
]
requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
traitlets = [
{file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"},
{file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"},
]
typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
urllib3 = [
{file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"},
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
zipp = [
{file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"},
{file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"},
]

24
pyproject.toml Normal file
View File

@ -0,0 +1,24 @@
[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."
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"
requests = "^2.24.0"
[tool.poetry.dev-dependencies]
ipython = { version = "^7.18.1", python = "^3.7" }
flake8 = "^3.8.4"
pytest = "^6.1.1"
isort = "^5.5.4"
black = "^20.8b1"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

4
pytest.ini Normal file
View File

@ -0,0 +1,4 @@
[pytest]
addopts= -rsxX -s -v --strict-markers
filterwarnings =
error::UserWarning

244
requirements-dev.txt Normal file
View File

@ -0,0 +1,244 @@
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

View File

@ -1,12 +1,66 @@
certifi==2018.10.15
chardet==3.0.4
falcon==1.4.1
gunicorn==19.9.0
idna==2.7
kazoo==2.5.0
psycopg2-binary==2.7.5
python-mimeparse==1.6.0
requests==2.20.0
six==1.11.0
-e git://github.com/alanorth/SolrClient.git@c629e3475be37c82770b2be61748be7e29882648#egg=SolrClient
urllib3==1.24
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

6
setup.cfg Normal file
View File

@ -0,0 +1,6 @@
[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88

View File

@ -1,9 +0,0 @@
from config import SOLR_SERVER
from SolrClient import SolrClient
def solr_connection():
connection = SolrClient(SOLR_SERVER)
return connection
# vim: set sw=4 ts=4 expandtab:

0
tests/__init__.py Normal file
View File

724
tests/dspacestatistics.sql Normal file
View File

@ -0,0 +1,724 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 10.15 (Ubuntu 10.15-1.pgdg18.04+1)
-- Dumped by pg_dump version 10.15 (Ubuntu 10.15-1.pgdg18.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
--
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
--
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
--
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Name: items; Type: TABLE; Schema: public; Owner: dspacestatistics
--
CREATE TABLE public.items (
id uuid NOT NULL,
views integer DEFAULT 0,
downloads integer DEFAULT 0
);
ALTER TABLE public.items OWNER TO dspacestatistics;
--
-- Data for Name: items; Type: TABLE DATA; Schema: public; Owner: dspacestatistics
--
COPY public.items (id, views, downloads) FROM stdin;
8337cf0b-4215-4391-8ba5-72f0d6532417 99 105
b41596f8-2986-4285-b409-893cb69eda25 799 101
dfb10a04-71e4-47d5-9896-a60ccb5ea025 766 95
b3a431f6-8da1-4efb-b162-feed75de7e6f 95 105
310b0efd-3086-4d27-9602-4e3f39a386ef 101 163
c24eec45-d311-4208-8009-ec5fe04a97c4 21 13
dc261c4c-9219-4419-a583-d60b93d12b0f 20 0
9c61859f-96b6-4392-a838-d0f97bb6e2e7 47 0
f29c704d-5c3e-43f1-ae97-8e06895b5fd7 156 0
3583e3ce-c2b4-481a-bb49-8c901bbd5dbc 90 108
20d6a78a-53a7-4f95-ac43-0cac094be09b 23 0
8fda532b-af3a-4099-a5b2-f79ab2476bf2 36 0
279a7025-3ea5-4d2b-9ffa-d8fb449945e1 152 0
423d54b4-533e-42e4-96ef-9a4f1b7c5717 23 0
5dee78a6-22d5-4c5c-965b-6c61d0baa4f6 26 21
eb4223be-18b7-40eb-bde8-faf26f350bcf 21 0
dc50b82c-29cf-42a0-b39f-eaabe8c20ab5 20 0
ef8cd486-ec2c-43f1-943f-aa77ce3d21c0 46 105
844180f2-9b7b-451a-81a1-776aaea39c23 120 108
8810a970-c2f9-4132-a0c1-841e0a08b35e 35 0
fd8a46d5-1480-4e69-b187-cd3db96d8e4d 105 105
8f90b3df-3db8-4f96-bfe9-989c62302092 28 0
6b8b0ac8-3b67-47b1-aa0a-5e3c91f37b7b 48 0
673c3414-ca87-4510-82d8-e59dea605c14 21 9
e0e42cc1-3804-4e5f-a220-87b87deb3f45 20 19
36830fe2-4162-4d58-a3bd-787ccb9f5163 23 17
69c501ac-4131-47a7-a682-5cac29e0d86a 28 8
976dfb78-9a3d-473c-aa10-ce6fe6580199 48 0
2adc5316-a1f1-4d32-b99d-304740bf7faf 152 0
31fd8f16-7f1d-4ba5-b151-3933cd74fb82 152 0
fac183cc-7002-49d4-a98c-61cef9fa1814 126 108
c25461c2-38a3-432c-aac8-dc2034c25b28 21 0
04a557ce-1d5a-4de8-a570-9a1fc3bee89b 37 104
2b54683d-1dd6-4608-85b3-d0fe388f2b17 150 0
07b41846-99cb-4618-a005-0a64511270d3 90 104
4faa192a-57da-4f39-8989-82e9a2b07c08 100 480
eb9cfc7c-9858-4639-b51b-d901a5bbfbbc 21 0
c2de3376-a7d3-45f7-b493-9ec028cbeda5 21 0
dd50bd8a-6048-4a74-ac89-c01238e6c2f7 20 0
dbeb85c0-2e85-4298-8c58-18686c7ee5da 21 0
a06997b1-bd19-4cc8-beff-62177f8f5e5a 32 104
f40c94b6-f460-404b-af43-c430a42357f5 272 0
8b6d7bc2-a383-4524-b39b-5d0a28669e05 36 14
c56d616f-336d-41d4-9271-3dccf375675d 101 104
beb785dc-fabd-460a-9728-00f880fcf43b 930 103
979fbd9b-ae0a-4bde-bcf7-2ea20976f5c2 48 0
f766f22f-d058-4dd4-b995-456962bea362 272 0
8e824926-9817-4422-9fd8-62ccba23309b 36 0
8cbd96cc-e999-464b-aa99-78f327b67c6d 36 14
dfc14a4a-e4ac-4542-b7b3-d9900fb3d18b 20 0
17263397-77bb-4b90-8fa3-3457301c0997 22 0
1767704d-ab52-446c-9699-a8c69352b550 22 0
00f7f75b-b6d1-499b-97dc-92ef5beee40a 271 0
8f0e552f-8c88-49f3-8409-37d0fa544048 36 0
149d5633-a43c-4004-8b55-efe5822ade71 271 0
1b59bdd8-3fc6-4956-a7d4-70a453a45b3c 271 0
e53a2eab-1e31-448d-907b-3656ca4e86c1 11640 0
423b7a6a-c690-43e2-af07-ce65d409f821 22 0
09b356bf-03e7-41b4-b338-6a8f50abb0eb 303 478
e0ada462-fa0a-4702-b9f8-bf2d5b32cb16 20 0
4d2aa64d-98e4-4187-b342-507af0e0d441 10996 0
551a691b-61a2-488a-910e-a6911f6b2372 730 107
dc3fdd43-763f-4509-b9af-3fa55e74a24e 21 0
336e416e-b14b-42e2-aedb-e4d6baffed3d 119 107
9830e938-25cd-44d5-b1e8-4c2ffe2a5631 48 0
5c3cc6f9-5196-4cf2-a449-4e2aba5f7a72 50 107
b4ced40a-45ed-47d2-9069-0839584a09d8 887 107
c85d1ce9-a4ff-4bd9-ae88-3f150bb8e6ab 2111 107
9495aa8b-528b-4f01-af9c-4f49e44d9b68 540 476
8d52784d-ee5a-4d2c-8f4f-c9e796002ba9 10485 0
99828091-801b-475e-b2ce-140f2cae6e23 48 0
9c622801-1634-4bfa-acb6-2ec7fb706de0 48 0
72d5d4ed-b840-4990-8b6b-e338df2d4192 60 0
1725bfc8-e6a2-4e10-93f2-f0b7130d2664 23 0
387b01fc-83f8-4cd8-a0ed-bda8aaf28f7a 911 472
40918d3b-65fa-4df0-b84e-1e23a0df0c59 9640 0
6d8fa194-49d9-4f08-9b0b-dfb6d6c49c03 24 165
ee8e3a6f-dd01-4e68-877a-33b04914f779 46 107
6ad9918f-4360-444e-b18e-ea2cec4454c8 47 0
b03bbb1e-e5ab-461a-952a-94a27bc37dc5 256 164
eb749418-1fa0-458c-938c-44d739a90f9e 20 0
03be08e6-dee2-4700-a856-d38778c777f8 107 106
cb61bbcb-1e53-4538-ba12-52a85d0763de 9381 0
eba158d7-0904-47f5-94d0-8e1292c25586 20 0
7d45f9b3-3db9-4a58-85db-3b61ff3b4b22 60 484
64b2b5eb-8544-4e58-955c-97b7cbe2314d 159 0
176563cf-14d4-48f1-9641-898700e4b4bb 23 0
1f03daf5-1006-4d80-b336-8196b1249b37 23 0
1ff7d1ae-ff8f-43e5-b960-17c30645991d 23 0
20647378-1355-4b79-986f-af32b36272b6 23 0
dea2a613-1418-4d69-8048-5e20626f1c56 21 0
1a9333f7-5643-4914-9166-7cdf94b5ae8e 9356 0
7368ab13-ca09-4000-baa4-bd6d9fa869eb 60 0
5ca5a00f-d33f-45a5-b722-3bd2fab40fc3 22 0
dea48314-9a57-4bd7-80b2-170dfd55aa3e 21 0
668e8301-49fb-415c-b89c-f87347bb8660 22 0
171ef40a-5644-4543-a5cd-8025fdbffe08 21 0
66ad8ca9-6ac8-4404-b085-b548d0e04a67 21 0
66de576f-256d-4289-8013-c1d43e195727 21 0
6760a4e1-bf9e-4e54-aea1-c3bf9bfda829 21 0
678f0197-0262-4a72-ae27-c4ed73158420 21 0
df87ff33-39ec-4bcc-b4d3-f3449bd04ff1 21 0
dfb812ba-c4f7-4711-a1c8-cf99d5555b4a 21 0
b6022981-e988-4a50-b605-ab9f10cd32b2 84 164
e0a61160-30a2-47e7-bf3a-4e4af1205c8f 21 0
1e9250dc-5917-476c-aac6-ff3092ad104d 145 163
21eca204-3396-4ccd-b4b6-9e5b87c4e3a6 130 163
7427d4c4-59dc-4de3-8fbd-60c659d685e1 60 0
7618c96d-77af-4bee-ac1e-ae073e8fce36 663 106
9dc15ade-7f6b-4fab-9216-0042d343983c 93 106
d027230d-c658-46ee-bad2-96e2f1f8c852 112 106
8f112009-5608-402d-bc28-84cc211a0347 36 0
8f2b2957-42b2-4a63-b6a1-5d66e63f97b7 36 0
208107c5-2872-425c-b535-04daf324811c 23 0
20ab614f-f3e9-4f31-be71-456cd76b090d 23 0
20b2223b-fb0d-4211-be34-50f1ee61a010 23 0
d6728884-c1ac-4ea4-9e84-4b8b4f8bda16 122 106
f17e3015-400b-4e86-a96b-d572494d9d83 101 106
f9a8789b-de85-4f6c-a960-74cbacab20c8 34 106
1756a2d8-dda0-48ab-bf54-0872c0858407 796 105
9177ac0a-94ad-4283-ad0d-9e068e981893 936 100
451a7398-accc-4a66-b516-9b7101caa8b3 23 17
01b763a8-6c81-4f93-a684-e83580365045 782 448
89da6f9e-b562-49dd-ba3e-d14da1994441 114 103
581bf8c4-1137-415e-943e-d15d667fdc06 21 0
9d5cea16-6342-47c6-ba9a-50002c1aa358 35 0
a261fe89-67a3-4222-b095-db45f4304c9f 35 0
79b8ee1b-b7cc-40dc-a773-e4d5ef21e176 60 0
03537c42-59bd-44b8-9aff-c16b884bb75b 20 16
7412cf4f-e2e3-4410-ae69-976d82fe3af8 8733 0
c60caacd-4022-4224-8c14-cda1fef5b36c 8514 0
9453970d-863a-45c9-9b33-d56383fea749 122 103
95848fd8-4498-4bc8-979b-545058db5971 138 103
9ff56907-3bd5-4f1c-9dec-58be0323c641 34 0
62bb9be7-c70f-4930-bc14-9a12deafdfe1 23 15
037b7851-bb11-4cec-b32b-e98466301a51 20 14
30614b26-7f5b-42f7-be64-a89b544929c7 130 448
78782d63-4ad0-4f22-a2c8-de94528df0dc 59 0
ec091486-c315-46f1-aff1-840cf857acf0 8376 0
ec32de90-c451-427a-af0e-ee2dd4a88d3b 21 8
cb09f770-49c9-45ff-b2ac-0ed237fb1d57 99 103
be93ad69-4a1b-4ef3-9749-b35ae25ed448 48 0
c0d4cd82-a9bb-4051-80ae-ddb34bd83594 48 0
dee1a6ce-2b0f-4661-8125-0a9bd4284d7e 29 103
42d9bbfc-f93a-493a-81b5-cecc6599ac5f 23 0
02ccab8e-1ee2-4d63-a54e-6659bec867e8 20 0
516972f8-b236-465c-b3f8-29d5aa73e041 8205 0
43970018-06fd-45c1-bf93-91ba5414eec5 23 0
e1a20b8d-f002-4879-95bb-a023d5f7bb97 435 448
43ac82a9-9ff1-4312-8240-f1c31a1bafa3 23 0
243ec3f3-9399-4a90-946c-7cf09e73e182 151 3
a4330062-076f-45d6-b475-70a991d7f6a1 6850 0
9c4a2dfa-0ca3-4e69-8c04-701ad115a8d2 271 0
44abc402-e849-4152-bd92-9d5c61c73114 23 0
9eb7f500-2b04-4235-b0ea-28dd2999d08f 271 0
2ea52c46-a231-40b1-81f8-3b82670ecedf 355 447
9df3ba3e-f94d-4d30-924e-e549a66f6372 36 0
032d103f-2f31-4c71-85a3-daa421d5b434 20 0
9fa43c24-1a51-43c1-8c79-13920b3429a1 271 0
a0dd671b-14f6-4818-b4d1-2dd1feae67de 271 0
9e2cd8a0-22c0-406c-be5b-5c25733b1abc 36 0
6a588cd4-646b-4709-bb26-516ea180f59f 6773 0
29d69597-5870-4c41-a440-6c856919a907 153 0
9ebcd5a2-c0c9-4ff8-8617-6490573f7172 36 0
78f7d9e9-4ed8-4d28-b834-973da35363a1 513 435
81d128bb-b984-4323-9dcb-53ebeaea9285 6249 0
2815f1af-7395-4ac7-a357-824b4cc843e2 41 102
90e27924-ac10-444e-a276-33b4f99d02b4 152 0
03794161-83eb-4ebf-b621-52c92dbdd06e 20 0
482dac0e-04ea-4d69-b6cd-e0a6d56393f3 23 0
969ca2a4-724f-44e3-abe6-cde850b8924c 152 0
351adb1d-d76d-4180-ae08-e7286590fd47 3923 0
96c0e2f9-ee33-4029-9db4-001ee5073df8 152 0
4845527e-baf5-460f-9239-b6250ea14e83 23 0
b2aa40b5-df0b-44d5-bc99-ad3debe15455 97 105
a644cc8a-5518-4bee-9e9a-63c259d1c466 152 0
aadbdc6d-9cfb-4f9e-8956-7ed6a1e41ac6 152 0
15b0d58e-4d42-413e-82e9-576a45c6f0e0 151 0
50009828-ad3c-498d-acf5-a20e230fe240 81 102
042a5e78-2823-4e1e-a7e5-e2b97b35284d 20 0
f5f7e6e6-17f5-405d-9509-33d7921a3503 35 105
c77ee698-fbea-402b-b9b5-6be6bbbb32f5 37 104
ec934a80-d0a4-4212-8c33-927cd48402d9 20 0
54bedb39-fb71-4019-8467-cc08935705d8 23 0
57e4cf5d-02f8-40bf-b7ee-06f5ada5e798 23 0
583faa4a-612c-4258-846c-5ac307d9ed68 23 0
595da9c2-fe0a-4bad-b754-792184073532 14 102
a1d18e99-c782-40bb-bfe0-4d1aa8f4fe86 36 0
a45b6cc5-265d-4a8b-aa26-d93fa812858e 149 102
18855f6b-fb43-486e-824a-fa3c66f4caa5 150 0
ad60933a-73cf-4f6c-8599-beba974efce3 693 102
ec98ee2a-db19-4810-8b7f-4b1928d87ef4 20 0
1c42b61f-6ee6-4457-a627-f9a3769b3e3a 861 101
78202ae4-da22-49e4-a15c-a5d6b02697f6 60 0
785c4e09-e742-4800-8d20-8d7b5b3d8a3a 60 0
a31f10f9-e493-400b-ac06-38c5a4a2d8d0 36 0
2cfd4248-ecd3-40ac-a08b-69fa1d886f59 150 0
c8a2c306-86f9-471b-b476-2512f4d13627 38 104
2b6a39e5-bcfd-45f9-ab3f-46d9b09a2c49 30 101
a34c5e0e-35b3-40a5-afd3-b7f7c149153f 36 0
eca74603-615a-4e1a-9616-18daa763d61c 20 0
34d05b2c-e2ef-4a6f-b08a-f9ad4faa0a87 103 101
cba7fb71-9709-47f2-84fb-35d1c3e27e80 122 104
ed874edb-3b82-45c6-9729-13fc3bdc3246 18 0
43145bd3-6d8a-4a9a-bee5-9095a2a00d7c 22 0
43413a21-ad21-47f3-a9ee-a3c30cbed3a1 22 0
de8f7105-e643-4038-9fcc-197e05324638 143 104
43df80be-ad8c-4f83-b8f4-80ec76591bda 22 0
22943e07-9f96-47dd-96ff-8e4bc5ec8016 413 465
3f85119a-4790-4a66-9176-a66d26c6c6cf 831 464
35e825d5-0e95-4574-90c7-2b28a49c4284 109 101
e423397f-0bf4-49d8-a3c8-0aecbd5be9fd 625 104
6ff423b9-c2a3-44f7-b33b-bcd284bd19b3 478 464
beddf86b-bad2-4662-8111-bf7507009a89 153 452
d0abb4ce-1a0e-4b07-b7ef-8d114baca29b 866 450
51f5da8a-30ab-4903-a3ed-b9dcb8feb3ac 22 101
556c4e98-7e6b-43ac-aa67-1e0b5de8f62a 122 101
7060b8eb-71e7-43a7-a1ec-c78cb3975aab 115 101
785fc0c8-771c-431c-8149-51bf112430aa 60 0
100ad959-fb82-499f-844c-520b283ff197 148 0
786547ef-572b-4089-9b58-d67cb515518f 60 0
e49f3ff4-f7d9-4c4a-92d1-7b37efc36956 108 449
a383ffe9-a07d-4d23-a78e-1f721143f9cf 36 0
a3ca562e-bbb0-42dd-a525-bfc28cba5ec0 36 0
ebb8be73-e23d-41db-9113-2d3673cdef10 469 449
78a9b4d1-266b-4f05-b5ba-89f6c0f61859 60 0
ed96c5fa-9e3e-4370-919a-e550e5c20030 123 449
729db178-fc6b-4486-90c2-cfab05c616b0 129 101
7b7ee26b-e4a3-4892-9b74-c60b8729112d 96 101
5a0db640-75fd-441c-8ccc-50b47b4f975f 1368 99
e037a86a-b782-4b46-b5ef-129c2d6f92f5 139 102
5f330cc5-ff26-40dc-8f1c-b451b0f968d7 94 99
870a88a5-20c9-4aa7-9983-809586135281 59 0
7ae23d05-bdc5-45f3-888f-c225be7dc4cb 23 0
af3a1f39-b479-4093-8600-eeaf63727904 36 0
9ce1e678-9bfa-4a18-a472-3641b5361cb6 40 101
87c21063-8c53-4642-b08c-525e809362e5 58 0
a5a3493f-ffe9-4575-896e-f9e35e4e5f02 118 101
87d78454-c350-40af-bd1f-4da5df0f664e 58 0
eb61ec1b-693c-4ca9-b5d4-e18a579089ba 48 0
aa5c90a8-095f-4d8c-bf6d-50254c8b77f7 43 101
46d8f4d5-2cb2-4066-ac5f-8b99edc8914a 113 22
0d8c40a2-d22a-4ff1-bd11-5a0e63a13fad 18 0
c404b33f-37f3-4ce8-8068-842c6672eb82 5380 0
7bd74829-f479-49be-823e-2c12dead41eb 23 0
afc1e03a-cac6-47fe-b793-65d0eb9aee59 46 101
5779a03c-af83-4389-8cbb-2812942b02c7 35 100
66e5f965-3d28-4dfa-b86d-b321c7f12959 23 16
a220559d-98cb-4592-9d29-ae4b9d36d15d 5203 0
af46b3ec-11d5-4f25-a77d-a12290d6587d 36 0
e0c9b7a8-bc6f-4a92-98f2-f626a09ebde5 47 0
c179e8d5-7bd2-45d7-99df-618a365f0149 4639 0
62ce11c2-5a77-4762-9f16-b654556d8148 22 0
671e90a3-f913-4e36-b7ed-cd361cb18e59 22 0
aeaca89f-7213-46e3-b5a1-03c9f08fc50e 991 439
d4c2b444-148b-46f3-8181-f0f95beea71c 41 101
08f31ba5-58d4-42cf-b684-6ec234c45c83 272 0
f221e2c2-d326-4150-8fa4-6aa1ff3999cc 92 101
0d408d7e-e902-42d0-9499-fad9cf53a2b4 20 0
5a2709cb-6dd5-4e28-b3ba-4ee62ace084e 3610 0
841067cf-a457-4670-8b0d-86a2b1fac9de 60 0
684a6358-5971-4206-a36d-26a53de50765 23 22
2d9aeff5-686a-406a-b6ff-492282799b1f 573 437
175160df-29bf-42b3-a4d8-55d14df85105 1701 0
f13c62d1-aa6c-4cc1-a380-8f6255da5a51 1688 0
668281c5-8256-404a-abdf-c24656ea9ed0 108 100
e95e5359-d9cb-4a6b-a8b5-f41f1e70316e 50 0
e2329717-5503-4844-bcce-7ff4568edcdd 40 0
c5d3dd61-b77d-48f6-97d8-294ce2705205 48 0
b88318b8-ef8e-47a2-880a-ea05c4231819 1627 0
41ccb381-6c74-41be-b980-5ee8f6add15d 1620 0
05d6a9f4-d920-4e11-9f37-185f00434a81 19 13
989161b4-a638-40c7-9a2f-08729fc31ac0 3591 0
e0e7bb1b-8a72-4230-a129-78c3e1d3fc23 48 0
8818451d-0b1f-4138-8f74-047d23d63514 61 22
acc1ee3b-28db-4bfd-9c75-58e2b8963e07 36 0
7bfb4dbf-5614-4bfb-b275-2b141e5fd280 22 0
7c844c1f-0cb2-4606-b6de-51d5463f75ca 22 0
fb3f7b71-d0ab-438d-80dc-733fc4d8b821 36 101
44f70e70-1e46-4def-812d-fd3d63e5bed4 34 100
e0e8262b-44c0-4385-9de5-dba15e41f835 48 0
66997b3b-3107-4d09-a90c-8712477fbb58 21 20
67442c8c-3d07-465e-89ae-406c1ccc87a1 23 0
7c3eaa29-bc6d-4fca-ae0c-ea6adc9f3674 114 100
67654d19-2756-4118-b86f-5d48a8ffbe75 23 0
8031f0bb-c103-496b-bedb-b08d46f8cfd9 129 100
3c2ed0dc-ab73-4ccf-ab4e-6a4e81fe83eb 151 13
679fa328-a826-4f75-8c80-3ab091751f85 23 0
86e12c45-8ea0-4db8-b7c5-1c575a3b1f91 60 0
870216f3-56ef-4a9c-b6dc-94a56d9565e1 60 0
8703ca7d-f06c-4265-b59b-0d1b5fcede14 60 0
0e1fd2c9-7c44-4d23-b492-ef1ab1b2b068 20 12
67deebe7-74c5-4b97-a430-770b3732b513 26 20
b345055f-45f1-440b-aed9-0d9c43992e82 902 439
6be1f2a3-885f-4cc1-990f-305a3219c769 628 437
33126c81-8203-49ff-ba4f-47e4b8de8279 3562 0
adf32acd-aa60-4c2a-908d-bff90bd95206 36 18
8c2aad9b-2eef-43e2-a45e-15e10b160adb 108 100
0e24d0e5-01cc-41b0-954d-a06193f9c082 20 0
67fdf6ce-d106-493a-83cc-a4d6938025b4 23 0
bff64936-8dda-4909-b645-aaec66131b9d 806 439
87988c94-9600-46b7-b222-07590ac9e62b 60 0
0e812aeb-c5a0-4e8a-8875-31298460b162 20 0
32e229bb-90f5-416c-be45-3ced6450aab6 270 0
bf4fd348-e6d4-4012-8333-5069496155a4 651 437
0cbe5b09-53f7-4531-9a27-717393d334fb 20 0
e829b80e-a713-4a4d-96b4-db244f88c3ab 48 0
6b5e2bd0-ebf9-4007-a3eb-5f104a853877 3542 0
cd8f33da-d579-410f-9f9c-27859bf49d85 570 100
3c6af6be-f937-46a3-b77b-f11103bb9043 151 17
acfa1e74-5570-47d8-afb3-ff3974f67581 36 0
ad0459bd-ce59-48f5-9eb4-36ace78c242e 36 0
ad694528-5a2f-43c8-98bc-e0235f7c4644 36 0
ad837942-ee3c-43fa-9af7-79a205ceddef 36 0
ac53dd1f-0576-4f74-8684-2490ca610957 35 0
7b252429-4c78-4895-b93c-9cd4a3a34e04 23 12
cef4ceb9-190d-4f96-8bdc-4ad8322db438 713 100
af044c19-3edf-480e-b00f-dfb888dfdfed 36 0
c5f010f9-7c15-4265-83d7-846b7d9ee210 120 439
cbca3c30-4ced-41ec-8991-27f1914e52b1 120 439
f80e43df-3dbb-4034-a74e-9242231609c8 2050 0
d7daba67-471d-4f96-a1dd-acaf651b0040 148 100
0da94bbe-cfca-4670-add8-169e67f3ca92 19 11
ad236989-67f5-4f0d-8425-c64bd942cd42 35 0
b0b19338-b80b-4bda-94b5-722b21403e44 35 0
68435ca6-34ed-489e-8a4d-9beb54720257 23 10
0e8b6641-0a7a-4174-86a7-ed08830a0310 20 5
b01962df-52d1-476b-9ba2-d26cc9f13945 34 0
b042919f-8167-472e-884c-658f6ce103f4 34 0
0f0ccbc9-c0a4-41ca-b2c3-5fc3fe00db35 20 4
686b3ad5-0815-41f2-bf75-6486c43112cc 23 0
68a128a2-04c1-4742-a3b5-4b596aad3fe0 23 0
0e9bee77-d2e5-4415-abc2-40481eea14af 20 0
f6ee0bc7-e975-4f8b-a869-7332a31fe9a6 943 439
363b87e8-6029-42a5-becd-75c2354ef7fb 151 0
0f0b6276-783d-4701-9489-647fb2246a64 20 0
0f125327-3c86-4941-bda7-e4b5783dadfa 20 0
1d9de7f5-ce21-474f-8499-e23ab8f4ce7c 467 438
4f94f745-7342-4d9d-8d7e-88c1ff44f86e 563 436
33e97348-fd37-4602-94bd-d9d91083f195 150 0
7bdc72fe-dc2a-4670-8ec5-1e57c0c83451 742 436
8801a829-4752-41fd-b3e2-c9b6963011d0 60 0
882f9d8d-bee1-44c1-ada9-062044414a51 60 0
e83aee5b-e722-47bc-8672-163cb1a7ab38 48 0
b0252cc2-d1a8-4149-b153-632c8ca38c7f 36 0
b07cd452-1eb2-4058-abe4-b3c2dfd1a2ab 36 0
b0f9f4cf-ec60-449f-a2c8-a763414f9252 36 0
b3c70b7a-fa54-4aa2-909a-96ae3e9b8318 55 436
126d7086-315f-4961-a099-7b7414585f63 2011 432
a298b29a-3896-4e35-90f1-d0f0ab664656 63 0
a9b967ea-312b-4bef-9d2b-73e98907e5c8 1273 0
ff193162-53bb-4d15-8db3-825efedbea55 39 100
25e4a4da-0c5e-4be5-9993-d4120a2ba29f 4613 0
84edbbc2-ed75-4c82-9414-44312e42ca3a 22 0
db7ca8d8-73db-40cd-8b9d-8364aea6f952 4512 0
30e51c45-a29d-40bb-a232-1078bcaf7974 96 96
4a6102b5-dce6-45a0-9ad8-868ecd984b97 1355 0
19c40e1b-52a4-488c-a813-aa12f28f184d 20 0
d78c4d55-193a-48bb-b726-077ad0ddc627 107 432
a02ad929-dd06-4bcf-ba51-9df34b1d4ff6 60 24
1ac49a9f-a3b2-42b6-9ad1-155632ac61c6 20 22
0637167f-6824-492b-b8f9-9a1f10430f6f 115 21
21e2d1a2-4af8-4f3a-8315-a20db534b725 20 0
84ec8b0e-17d7-4182-92d5-c9ec0e325a91 23 0
19e7d326-5a64-457a-b946-6e65ba6d0c73 20 0
015b1c31-7ba5-4b41-9a12-c3339f3849a1 1299 0
b2eb725a-e83c-4afb-95c7-f39a97d06fa9 36 20
1a21765d-e704-46dd-8427-c3e0c40a8848 20 0
8683e69f-cc70-4832-b3df-3c9f34f959df 22 0
87cefbf6-72db-4b21-8fcd-c9571b385076 23 19
a2e3d17f-bc42-404d-9c14-4e2645496450 60 0
870bd8c1-ed5e-46fb-ae30-4bc0060faff9 23 12
b165ea5f-23b0-4dc8-8fda-8db1e34d19af 36 0
6fef266d-864e-42e2-9b2b-a82af8f06f07 150 427
b47d678a-4c2b-4530-a5c9-4e671ebc3d93 36 0
d349881d-911b-45ed-b82e-ff38b8853076 163 415
1326b307-6307-4e17-9b0f-bebc4b6195ac 1275 0
29c2a7ca-7d55-478f-99c2-5a65955bba41 146 413
190c6eb2-1114-42c8-8249-69bf029db7ae 20 17
1962c06c-d522-4014-92c0-964c7bea8b42 20 9
1957180e-b11f-4dcc-82a6-48bbb02c65b3 20 16
1951e01e-f5d3-422d-9c66-cfcd03a11316 19 0
d2534dbc-9fe0-423d-afe6-d2a38ae307c8 567 423
8785a83c-137b-4fb9-a4e5-ce33410beec1 22 0
b1edb8eb-d1e0-4881-b485-654ca1a42466 150 8
a204e344-b253-4f0b-a457-d167765a88f7 52 0
67ef5438-a467-4e4a-8574-70be5826952b 116 99
74ba7e9b-e3df-4ede-8082-deb0e6a732c3 135 99
b293e4c6-e20a-4050-9504-25dc97ec9401 35 0
7dcf1710-45f5-48ec-9a9c-bc14ae6d930a 1421 99
19a9f5b5-ea6e-4770-86f4-d82a6f8d9bfc 19 0
84fd1f0c-8ec9-460f-81d0-39e272a11da8 23 0
691a4f5f-339c-45c7-8eb3-fe3fb51dbc21 1619 0
a17c0e5a-3e07-4c87-98cf-4209e859dee9 151 0
2bb30f59-0cc0-4103-b9cf-49a7d20bc2ed 195 413
86b1d0d3-f175-4cd7-83b3-ce82f636c2cb 23 0
1d2762df-007e-407b-b57c-94e0d2669449 19 15
11ec7bda-8787-4e88-9674-89cce182f2fc 20 0
211662ce-584b-4967-a061-c000c4f5b572 20 12
8fc7715d-2028-4b38-83bf-6554086c2f55 1385 0
e4fe3574-4fee-4483-aeb5-c1280d4a3930 359 413
133bd6da-7290-4fcc-9036-bff03d3b398f 1383 0
99aa6606-c519-4e40-965f-2fc327a65749 1378 0
37d1c66a-6ea0-458e-a8e3-7b90c58eb2b4 1359 0
f7ca960e-988a-493b-8444-23c2010dd378 1358 0
213e9145-c764-47f2-a0ab-c28e54169457 23 1
a004917e-f262-4169-97d9-75d6766e2a1b 60 0
fd44c06f-c8c4-4cc5-9ee0-f3b0c66b3641 1358 0
0cd49d01-2137-4a51-9183-47ebf6df1eea 46 411
85cc419f-7322-4448-b6d0-1bab0e4cff23 28 99
b394f451-9ea9-4368-8839-f7e844e4b610 35 0
9e2bec3e-8899-4759-9e03-2f84db61e7b9 36 99
cf0d2a88-43a2-4580-8d08-acfd1ae1db2c 28 411
c8cb4004-43b3-460d-9e3a-180832dd3e80 38 99
34f1d417-1bd1-4bf4-ba4c-b119f7c8d0db 269 0
b20bd200-b17c-4ec9-8643-b2f3b17b80ee 36 0
b4c3cbad-fa07-49f5-bbc6-974fbf8fdd0b 36 0
a00cf6cd-4629-41cf-bc6d-564cfb665f11 60 0
b29f474a-b1a8-408f-ae0c-64f0b0213696 34 0
872ee3ab-ffff-45b8-b171-fa2058eea33d 1095 434
c4fe19b9-b599-4422-952b-226a92396e26 391 422
88c04adf-4cf9-4e29-ab0f-9110eb194e92 1063 432
556ddfa2-1d09-4543-b19c-43e0d984bb45 1618 0
86cea6cf-d9b5-48e0-a59a-15252ad2d253 23 0
cc58c7ec-5b11-4686-8573-034a019a7706 865 410
86ed5177-4e77-4edf-810e-a1e69fc39276 23 0
4549182d-c4e9-4bf1-805e-7db761e3e836 269 0
967f2b4e-00a9-4739-b267-2d2010af7010 60 432
a10d56be-8597-4a28-94e8-fcf3ff10069f 60 0
cc82b0bd-153e-4c2e-9546-6b6386491a3f 664 410
879e3cfc-185e-44b8-b5a7-1ec14d4cab3b 23 0
6b6f882a-c704-4040-999d-931a85f0e66e 1605 0
5fc0a5ba-999c-4637-b61d-082c903225e0 1599 0
822f5a41-b9a7-4b20-a9be-a8652fd317ed 1593 0
daea4559-e3dd-41e4-b15f-9f1911cc685d 16 99
02fd03e7-1bfe-47d9-8f73-97b1a94a8fad 120 98
30019311-1675-49a6-b8a6-989aa822dace 1275 0
3f55df5c-290c-4e85-a841-6ace0c403c23 29 98
b22668e8-6f3f-43a4-ba2b-c47b4d2467e1 36 0
b29ef876-896c-4e66-ac0f-79f0f638334e 36 0
b2ae034e-3a06-48dd-8af9-8453d39cd4d6 36 0
193479e9-2fa1-4c29-99b5-37da8be4907e 20 0
514a3fd2-b3e4-41b0-96a4-30d88c28adf3 110 98
87066c49-5709-4418-ae2e-a5afc441625c 23 0
99f7b0cc-6ae8-4b9b-af9c-f373da343f56 149 0
0f1fe81d-9af8-47b8-9757-1c6c53de09e7 494 408
87f7037c-d743-44ff-a65e-4e60acd484f6 23 0
1276c6e2-44ce-4e91-819b-53741dcb5b77 161 407
87f12b8b-92cc-43d9-bc93-37c0cc7d6000 106 98
94d8271c-a6e6-46da-b76c-a67c427b0ad3 21 98
a1bf6b39-11cf-4963-a52f-c97b4eb5551a 60 0
c341d240-0839-4ab2-afcb-8bce8b9952da 112 98
b3ae8dd8-e384-4dd0-8b52-972d7e2519df 36 0
b3b7983f-d495-49f9-910d-a0c814675fd9 36 0
b3d1b58b-a9bd-46fb-abbf-fb79dbf0811b 36 0
88661166-0db5-4fe2-a322-d65105ea62a3 23 0
888bf3b2-f829-4938-b0e6-97d0e21659fe 23 0
89091843-a159-4855-8dbf-8edbd13df395 23 0
d6455661-4550-4967-877e-9aa9ea99555d 88 98
e527af77-7fe3-4dfd-80a5-9f6de96173f6 140 98
f7162c3c-035c-4266-8504-5f05b361f1a8 94 98
1d043145-4d44-447b-88ce-fb96f839b74a 33 97
373242d4-e2d2-4d0f-947e-ac99570268ef 101 97
3e8f24d7-4a3d-4963-bf7a-3eef47d2c96e 96 97
4b962ab9-2903-4457-bd1e-4abf0b0ed4b4 126 97
4ce67d92-efb5-4ae6-bc20-d1d700d0ac90 140 97
946d8e74-848f-49a7-bb77-eaf786cc1f6f 150 97
c61adf2e-03a7-43bb-a3ac-31cb2b10eeab 108 97
b5b69f34-53aa-4b1e-8256-62576ec7d2c5 35 0
b6f0af75-6d29-44d3-8967-7d90831ab1b7 35 0
03e06350-1904-4e69-a99a-d2177c4ff196 46 0
af360f36-c65e-48c3-899c-03828f4a2c19 431 409
7cda54a5-fb68-4cc3-8684-a46ec88a6a22 146 96
a99f7cea-5b5f-4d56-a010-20d6580c2253 36 20
fb3f4b09-b8f6-4874-aabc-2fa2b14b77d3 188 408
8feea3bc-44f9-45e5-bea5-9471ac78bc77 23 96
ab83e77e-2fc0-4522-baec-043e3dcbdc76 62 0
b7348d97-8b00-4580-9a78-bb7433a498fe 35 0
1fd8779a-5b50-4a78-a828-fca4e28f6b67 1075 0
c454616b-db92-4902-af57-bc92bd31ec12 35 0
58c7a06f-2485-46ed-b804-95c6f8a56f1e 1075 0
7864a913-d482-45b3-b90d-d15be0119f78 92 394
c8c82563-52da-4547-98cf-7212fe580881 35 0
c180d0d0-bbbd-4c78-ab5b-c68917661d61 266 0
ac1b8e5d-1cfa-4dac-b1b4-99038cf7cff2 60 0
6988d9eb-dca8-4bcd-bbaa-17bb7c08a537 1042 0
1a9feed3-b3b1-41ee-9028-639430fdf084 43 0
c8cebc09-ee8a-4a0a-a410-c028871828a3 35 0
c9ae6be2-bf64-42ee-8ad1-ebcc38b3f7fd 35 0
9189cc1d-bd21-458f-a491-5d98fcd743ab 127 96
b59a3b2e-2ab8-482b-b3de-8725d5962c88 34 0
ada42f04-e584-4e7c-a08f-ffce4d1f92f9 60 0
e57b45a9-809b-4222-a009-4517db2bc267 120 96
00966752-fbc8-44a3-a4c6-bd0dc8c378bf 93 95
ae32bf20-0a09-48f4-98fb-0d4a994b812c 60 0
c99270f1-46db-4bbf-a8cf-5cddd89fb092 34 0
2051c6da-d6d6-4ba9-9504-9cc4b516b11d 543 89
f3a88950-3f1b-4e89-8e3d-64e54f785e45 151 0
89477009-4071-42bd-86bc-72cd713f8a52 23 0
c2a7f1b6-f2ec-4a02-8878-3c678860747f 39 24
add5f380-f257-4a74-8b8e-6523ca708516 52 0
fa906948-fefa-42d0-b205-c2e015731fe3 151 0
69459952-e4d7-4695-9618-a06c835c7e8f 57 393
01d64f82-8c01-4844-9d8c-1d8ca4ba70cc 150 0
b647f4a2-d330-4f4e-a612-ed98789f97d8 43 19
0359923f-f240-40ab-9e45-b7bbed25ee27 150 0
8990fe0d-a458-4e02-9ea9-184d878c9b9e 23 0
08eb737b-d49c-475f-96ab-d91ffc5daebd 150 0
15eb3671-34e9-4099-ae1c-ace48954b6b9 149 0
adaf0448-32b9-4843-b678-42cc428cb57c 60 16
0899c67f-1e5c-416d-93fc-00f46b4d8c78 148 0
04aef27f-e6e6-4400-8b38-e1ce3d230c69 49 0
018e7e6e-ae60-49f6-921e-d9767d0d9338 47 0
7049872b-786a-4ab2-b702-bdd362a06846 618 393
b260de77-9546-4f22-b4ac-7d6f030a61cc 23 14
bde9e42d-a81a-44d6-85e7-db41a59e02d4 34 12
bf47c417-c571-4ac6-ad05-fdea0357efb6 1272 12
6271ec60-7f15-4d63-a559-5c98c4a67cf8 25 22
832d081c-45ff-41ba-aaed-0d34d0f1c42c 20 393
bfd03b36-e33a-401d-a485-80134a4e62a0 23 0
0d03143f-f04d-4aee-90dd-27bc95e5e4eb 47 0
7cf1530e-c2fe-4bc2-8c33-1843fcf597ec 40 22
7f8755be-d684-43be-84ce-4da7939eac59 38 22
2dcab519-ae51-4c41-9eef-fec4061c60bb 4166 0
ab86493b-62a2-4621-a2d1-8efc74e7087e 36 0
c0324c41-c2c6-49c1-8a27-b6768749a811 23 0
ed42164b-f8db-4ec0-ba97-f45fb40196ef 4123 0
18489caf-e08d-4710-9c28-cacd8711ef2e 47 0
7e2d9e24-5a09-48c8-b32e-2767144b13bc 4079 0
71945960-2bad-474c-a652-c004ba8f4887 3965 0
b59997b2-84ea-4605-992c-6861dee12ef9 36 0
18a75de7-9599-4662-8af9-53c05623e9f1 47 0
18c855cc-5862-4962-8157-c2e45779fb1d 47 0
194cfefe-318a-4e79-a61d-e9c2136c1e29 47 0
aafafea1-e461-407a-8673-455de5b5eefd 30 21
1a3d3071-60fe-4231-8dda-d9dfcb79db5e 47 0
c3d5d8fa-8986-4657-a516-a5e9a43e2c9a 23 0
b5b07d5b-091a-4f65-94f9-98161f09c18c 28 0
c3df86bf-4135-47aa-b528-f2bd0d3f9880 23 0
b5c14b3c-a81d-4bc4-b936-4af3adc2fc0c 36 0
c9772b3f-c7ad-4d19-bd93-fc16f0002206 27 0
b6edb93d-ad8a-4dc2-93cf-ec5318554ebe 36 0
c46afc39-c72a-4a74-9478-c6636dbb5e10 36 0
acb383e9-145e-4092-a16c-c95f19f0c988 46 21
c9a66973-e6b2-4776-bdb5-577b3d4c4caa 36 0
ccd4e0c9-347c-4d40-b40b-76ee422d7e4c 23 0
ca103457-8d67-43b4-a377-b41803a31fb2 36 0
ca4449ea-e4c8-4758-ae1d-ebc44b0cb2af 36 0
caf3ba22-4ae3-411b-8ee6-63463240b71a 36 0
a14a5f69-d9d1-4a09-8af8-5250016993a1 1260 0
ad02b3ce-8498-4d47-8f36-e2b1a58fde5d 30 21
add41aa7-b5cc-4bf6-bbdf-b64b84744af6 28 21
cce7a757-a60f-4aeb-893b-d9e69bfe8109 23 0
31b09eb2-58fb-415f-8e23-a683a6ce57fe 1257 0
2701e59b-6a80-49dd-8744-8c30c717ef64 1076 0
ce14df54-7d93-4bf5-bad2-dbbd3515257c 23 0
7460614d-7665-4252-b09d-a9d33f96cd2f 1076 0
bcd32967-d347-4d54-b38f-8cca1b711445 269 0
89235288-e887-49a4-b3f1-e4a36e1a2f19 23 0
cd9a3022-0a53-494b-948a-c33aaba47742 22 0
25e5720a-fe7c-49d7-a606-cd6b57b68e4a 20 0
264d5ebe-613d-450a-9fb9-dc318fc1722c 20 0
56a76324-03f8-4edb-8790-abfa4cc4e739 47 20
e6119f6a-89d5-4ad4-8162-b259161bd430 466 410
bbf052d8-7921-4593-837f-f879851426e7 268 0
73122c6e-9592-426c-95cf-e423eecef3ca 198 409
9e8576ae-8082-4b9f-8b61-ddf39040f248 480 393
70ff46cf-0887-47d5-b7f1-16801dca1c20 81 389
d80043a0-f391-46cc-8e85-fad19ae183b3 1076 0
df4fe2ce-376a-43c3-afee-e5cda0f6def9 268 0
5727ffa1-95c6-4c11-807d-936cc05daec6 446 388
6c5d3411-ca44-4973-b0ec-9eb1b5cb647f 495 387
75b81c09-3919-4dd5-a2bc-b85af3ccb444 72 387
e2ff87a3-b938-494b-842f-2038f4b3c5cd 351 380
fcc5b9a8-c143-4eb9-8132-68c1731020d6 653 97
054f51a0-8988-4ca7-b948-0663ff6a8706 30 96
1a936e70-dbaa-4f2d-94e1-574070036e3a 98 96
4313c8af-0f35-4af8-9736-f2567b16a658 108 96
9df3bd29-ffe3-4b63-b432-3c382bf6dd65 42 20
854f240f-b025-40e4-865f-1f15633ecda9 267 0
902612d8-312e-4f34-bdcc-407fbe9228fa 801 0
9e759c53-b07b-43ac-a74c-01351cd07816 605 332
08b821bd-ae19-4aa7-aa23-0f625e90807c 800 0
e7b7267b-1ce2-4f3d-abcb-fac886a70676 222 332
0fec6297-76ea-4438-ace6-8bdac16d48cd 57 0
0f4cb67b-ee09-4580-a6f6-96a6c3f51053 738 0
f550e947-a139-44ca-9546-b5afe3b7e98e 592 332
e71b05c3-2265-4e18-aff7-84fd4a14bf62 53 331
cf1b90ac-5c85-4ed9-a30c-7ab5589ccd6c 36 13
d5b15b7a-cf05-4f9b-8abb-e754d02eb067 59 0
991573f7-5020-4c3e-bb36-f42d09511e0f 267 0
35d9aadf-dd21-48ec-b6d3-76121991bd06 19 0
cb771273-3d02-4be6-a63d-2fb300a9dcad 87 96
d5ac9ac6-49f3-4a39-a9e1-5057f81ad6b7 60 0
cd68aad4-6291-4575-a37d-ca9f0f5d169e 36 23
91df1242-2954-444e-b888-888b6fa34586 938 0
7f97e1c7-9a37-430f-b513-876aac2ac17e 108 95
aaade3a4-39ab-468d-858d-691f009d47f5 920 0
0e9026a6-dd6c-4b79-9cac-a9930b97516d 59 0
6ba05954-1cec-4bc2-9f4c-95847df12300 22 0
2bcd1417-f014-4b7b-ac4d-926f3195ba0c 20 0
299c29a8-c8e6-409c-adae-4ee4c1dd712d 47 0
771d71cc-1361-4458-ad0d-88b04bb70e0b 149 0
e9020175-3881-458f-9f06-059b15e99bb1 23 0
cba36f72-0064-461b-9b25-77b1ba80a470 36 0
b9d602ca-f1d8-4f88-911f-52324b770736 38 95
ceefdc76-9677-43cd-b4e4-e059f7b51d4e 33 0
eae41ad3-5655-45e9-b049-7f1b17f825bb 23 23
134c28ca-398c-4264-9b6a-90941cde61b9 22 21
0ec93c8e-959c-4530-a43b-9e5ad385aaee 59 17
0d466dfd-da83-4595-8a0a-bc64bef176ed 74 95
14294494-663a-4abb-8f61-c058b973b466 22 0
14f21608-7d36-460a-b34c-1154a1e941e3 21 0
40352950-2a1e-4d63-a26a-1f331c1ad3df 908 0
d6fb62fb-8c4e-4874-82a2-807bcef37ca0 59 0
f9e645b0-b28c-45b5-a2fe-b96f5f40643f 129 321
f60e5503-8473-4338-9118-df2ccb412fa2 779 0
4f1c78db-95bb-4d7d-89fb-e6f8408759a7 908 0
11fa68dc-25a3-4f51-bce9-1621c6f81275 210 95
d5e11517-9c8a-4585-afd6-ea10b3002b75 60 20
53d06988-053f-4183-b8d1-b1158f6171fc 20 0
d29041af-09b4-4610-8535-c53c8e790a6b 920 0
f49f7d13-9d82-48de-b36a-989a69a744fb 611 360
5e4655e7-b3ef-4562-98de-6686f80b513f 30 16
1a1b1883-ce7d-4c97-b541-f0d4627c35c2 738 0
f8078011-1f6d-420e-9f09-3407ec4a6727 919 0
f0a45b2b-5e1a-42c7-aa38-7bfde6e864c0 908 0
b40c66c3-274a-4c60-9347-496f1b8e9c19 621 359
dde919f4-fd66-4a5e-bb74-ca04dac293b5 800 4
e9577dd3-7e69-466f-bb05-d3058315b30f 23 0
3c52ada9-b13e-4d50-856c-791efd58fada 907 0
4371be67-b276-4e94-8255-6b80b4837b6f 800 0
7d878da3-bf74-4eef-9a3f-b8292f12c77e 738 0
82c99ec4-6e89-40ba-8a79-c9c56d3dd972 738 0
9090f336-ddb9-4b9f-8c4c-341f1c96cbb6 738 0
d2a32fc1-8bc9-4a22-bf49-94a354f7718c 1146 358
ba2edc17-fd54-47ce-8f05-41d201df1b1f 106 95
9a48860d-7f2e-40fe-a5d7-70ebc9f93bbe 23 0
dbbbc3cc-8a6e-4f3d-8c0a-0b739544d323 152 95
14500ca6-4991-422a-91b7-9aeeaa21cae7 22 0
f69d76af-8548-48bd-810f-158a89e072d6 3860 0
6b905451-38ce-4761-9ca3-0f0d08cc72cd 150 0
727d4d54-245d-447c-bce8-156ce5cc36b1 150 0
7a9a3310-eaf9-4fab-b4c2-64a8a39ccd3e 150 0
0364493b-164d-4a33-a52b-b56261208d21 1010 0
e88e6c15-bb7b-4450-b675-3b459321024d 31 95
7dfdb8ba-dce8-452b-a66c-64c4b8f63a13 150 0
cbf021bf-f349-4d1c-a6f4-76a8dfdd441c 36 0
ddb6fef5-9194-447d-b966-92649b623c54 939 0
a5f1a703-5ad8-43e3-8333-01f28f375964 937 0
eaaa58a3-5079-4013-84c9-9f7742c4c607 112 95
06b6c01c-becc-4b93-b306-b6d9742a1e70 114 79
0e396352-52d2-4807-b0f2-430202d2cbe1 93 79
f79539a9-4338-40be-a526-e737f4cdaa2a 116 79
4a262bf4-e045-4f77-a1fd-521b2100b319 32 76
\.
--
-- Name: items items_pkey; Type: CONSTRAINT; Schema: public; Owner: dspacestatistics
--
ALTER TABLE ONLY public.items
ADD CONSTRAINT items_pkey PRIMARY KEY (id);
--
-- PostgreSQL database dump complete
--

382
tests/test_api.py Normal file
View File

@ -0,0 +1,382 @@
from falcon import testing
import json
import pytest
from unittest.mock import patch
from dspace_statistics_api.app import api
@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
def test_get_item(client):
"""Test requesting a single item."""
response = client.simulate_get("/item/fd8a46d5-1480-4e69-b187-cd3db96d8e4d")
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_item(client):
"""Test requesting a single non-existing item."""
response = client.simulate_get("/item/c3910974-c3a5-4053-9dce-104aa7bb1620")
assert response.status_code == 404
def test_get_items(client):
"""Test requesting 100 items."""
response = client.simulate_get("/items", 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_items_invalid_limit(client):
"""Test requesting 100 items with an invalid limit parameter."""
response = client.simulate_get("/items", query_string="limit=101")
assert response.status_code == 400
def test_get_items_invalid_page(client):
"""Test requesting 100 items with an invalid page parameter."""
response = client.simulate_get("/items", query_string="page=-1")
assert response.status_code == 400
@pytest.mark.xfail
def test_post_items_valid_dateFrom(client):
"""Test POSTing a request with a valid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00Z",
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
response = client.simulate_post("/items", 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_items_valid_dateFrom_mocked(client):
"""Mock test POSTing a request with a valid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00Z",
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
get_views_return_value = {
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d": 21,
"e53a2eab-1e31-448d-907b-3656ca4e86c1": 0,
}
get_downloads_return_value = {
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d": 575,
"e53a2eab-1e31-448d-907b-3656ca4e86c1": 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("/items", 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_items_invalid_dateFrom(client):
"""Test POSTing a request with an invalid dateFrom parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00",
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
response = client.simulate_post("/items", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_items_valid_dateTo(client):
"""Test POSTing a request with a valid dateTo parameter in the request body."""
request_body = {
"dateTo": "2020-01-01T00:00:00Z",
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
response = client.simulate_post("/items", 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_items_valid_dateTo_mocked(client):
"""Mock test POSTing a request with a valid dateTo parameter in the request body."""
request_body = {
"dateTo": "2020-01-01T00:00:00Z",
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
get_views_return_value = {
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d": 21,
"e53a2eab-1e31-448d-907b-3656ca4e86c1": 0,
}
get_downloads_return_value = {
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d": 575,
"e53a2eab-1e31-448d-907b-3656ca4e86c1": 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("/items", 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_items_invalid_dateTo(client):
"""Test POSTing a request with an invalid dateTo parameter in the request body."""
request_body = {
"dateFrom": "2020-01-01T00:00:00",
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
response = client.simulate_post("/items", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_items_valid_limit(client):
"""Test POSTing a request with a valid limit parameter in the request body."""
request_body = {
"limit": 1,
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
response = client.simulate_post("/items", 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_items_valid_limit_mocked(client):
"""Mock test POSTing a request with a valid limit parameter in the request body."""
request_body = {
"limit": 1,
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
get_views_return_value = {"fd8a46d5-1480-4e69-b187-cd3db96d8e4d": 21}
get_downloads_return_value = {"fd8a46d5-1480-4e69-b187-cd3db96d8e4d": 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("/items", 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_items_invalid_limit(client):
"""Test POSTing a request with an invalid limit parameter in the request body."""
request_body = {
"limit": -1,
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
response = client.simulate_post("/items", json=request_body)
assert response.status_code == 400
@pytest.mark.xfail
def test_post_items_valid_page(client):
"""Test POSTing a request with a valid page parameter in the request body."""
request_body = {
"page": 0,
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
response = client.simulate_post("/items", json=request_body)
assert response.status_code == 200
assert response.json["limit"] == 100
assert response.json["currentPage"] == 0
assert response.json["totalPages"] == 0
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_items_valid_page_mocked(client):
"""Mock test POSTing a request with a valid page parameter in the request body."""
request_body = {
"page": 0,
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
get_views_return_value = {
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d": 21,
"e53a2eab-1e31-448d-907b-3656ca4e86c1": 0,
}
get_downloads_return_value = {
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d": 575,
"e53a2eab-1e31-448d-907b-3656ca4e86c1": 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("/items", 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_items_invalid_page(client):
"""Test POSTing a request with an invalid page parameter in the request body."""
request_body = {
"page": -1,
"items": [
"fd8a46d5-1480-4e69-b187-cd3db96d8e4d",
"e53a2eab-1e31-448d-907b-3656ca4e86c1",
],
}
response = client.simulate_post("/items", json=request_body)
assert response.status_code == 400