1
0
mirror of https://github.com/ilri/dspace-statistics-api.git synced 2025-05-15 09:27:43 +02:00

Compare commits

..

1 Commits
v1.2.1 ... v5_x

Author SHA1 Message Date
963961354b README.md: Use travis-ci.com domain for badge 2020-12-08 09:10:41 +02:00
13 changed files with 77341 additions and 4108 deletions

@ -1,6 +1,7 @@
dist: bionic dist: bionic
language: python language: python
python: python:
- "3.5"
- "3.6" - "3.6"
- "3.7" - "3.7"
- "3.8" - "3.8"

@ -4,27 +4,13 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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 ## [1.1.1] - 2019-11-27
### Added ### Added
- Configuration for automatic sorting of imports with isort - Configuration for automatic sorting of imports with isort
- Configuration for automatic code formatting with black - Configuration for automatic code formatting with black
### Updated ### Updated
- Run pipenv update, bringing psycopg2 2.8.4, requests 2.22.0, pytest 5.3.1, - Run pipenv update, bringing psycogpg 2.8.3, requests 2.22.0, pytest 5.3.1,
and gunicorn 20.0.4 and gunicorn 20.0.4
### Changed ### Changed

@ -14,8 +14,7 @@ ipython = "*"
"flake8" = "*" "flake8" = "*"
pytest = "*" pytest = "*"
isort = "*" isort = "*"
black = "==19.10b0" black = "*"
pytest-clarity = "==0.3.0a0"
[requires] [requires]
python_version = "3.8" python_version = "3.8"

208
Pipfile.lock generated

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "be968d3927117f9ac14b9a6f60d6147b2d57ce55f694f34ed6e53abcd2197823" "sha256": "5e3f3e80bd92780ab66a80a83156001e50362a0d6195b1a304a15dc0c645c562"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -18,10 +18,10 @@
"default": { "default": {
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
], ],
"version": "==2019.11.28" "version": "==2019.9.11"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@ -60,10 +60,10 @@
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
], ],
"version": "==2.9" "version": "==2.8"
}, },
"psycopg2-binary": { "psycopg2-binary": {
"hashes": [ "hashes": [
@ -105,18 +105,18 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.23.0" "version": "==2.22.0"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
], ],
"version": "==1.25.8" "version": "==1.25.7"
} }
}, },
"develop": { "develop": {
@ -158,10 +158,10 @@
}, },
"decorator": { "decorator": {
"hashes": [ "hashes": [
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", "sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
"sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" "sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
], ],
"version": "==4.4.2" "version": "==4.4.1"
}, },
"entrypoints": { "entrypoints": {
"hashes": [ "hashes": [
@ -180,11 +180,11 @@
}, },
"ipython": { "ipython": {
"hashes": [ "hashes": [
"sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a", "sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
"sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333" "sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
], ],
"index": "pypi", "index": "pypi",
"version": "==7.13.0" "version": "==7.9.0"
}, },
"ipython-genutils": { "ipython-genutils": {
"hashes": [ "hashes": [
@ -203,10 +203,10 @@
}, },
"jedi": { "jedi": {
"hashes": [ "hashes": [
"sha256:b4f4052551025c6b0b0b193b29a6ff7bdb74c52450631206c262aef9f7159ad2", "sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27",
"sha256:d5c871cb9360b414f981e7072c52c33258d598305280fef91c6cae34739d65d5" "sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e"
], ],
"version": "==0.16.0" "version": "==0.15.1"
}, },
"mccabe": { "mccabe": {
"hashes": [ "hashes": [
@ -217,39 +217,38 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
], ],
"version": "==8.2.0" "version": "==7.2.0"
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
"sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
], ],
"version": "==20.1" "version": "==19.2"
}, },
"parso": { "parso": {
"hashes": [ "hashes": [
"sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157", "sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc",
"sha256:8515fc12cfca6ee3aa59138741fc5624d62340c97e401c74875769948d4f2995" "sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"
], ],
"version": "==0.6.2" "version": "==0.5.1"
}, },
"pathspec": { "pathspec": {
"hashes": [ "hashes": [
"sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", "sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c"
"sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"
], ],
"version": "==0.7.0" "version": "==0.6.0"
}, },
"pexpect": { "pexpect": {
"hashes": [ "hashes": [
"sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", "sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1",
"sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" "sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"
], ],
"markers": "sys_platform != 'win32'", "markers": "sys_platform != 'win32'",
"version": "==4.8.0" "version": "==4.7.0"
}, },
"pickleshare": { "pickleshare": {
"hashes": [ "hashes": [
@ -267,10 +266,11 @@
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e", "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
"sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a" "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
], ],
"version": "==3.0.3" "version": "==2.0.10"
}, },
"ptyprocess": { "ptyprocess": {
"hashes": [ "hashes": [
@ -281,10 +281,10 @@
}, },
"py": { "py": {
"hashes": [ "hashes": [
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
], ],
"version": "==1.8.1" "version": "==1.8.0"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
@ -302,71 +302,50 @@
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", "sha256:83ec6c6133ca6b529b7ff5aa826328fd14b5bb02a58c37f4f06384e96a0f94ab",
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" "sha256:b7949de3d396836085fea596998b135a22610bbcc4f2abfe9e448e44cbc58388"
], ],
"version": "==2.5.2" "version": "==2.5.1"
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
], ],
"version": "==2.4.6" "version": "==2.4.5"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", "sha256:63344a2e3bce2e4d522fd62b4fdebb647c019f1f9e4ca075debbd13219db4418",
"sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" "sha256:f67403f33b2b1d25a6756184077394167fe5e2f9d8bdaab30707d19ccec35427"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.3.5" "version": "==5.3.1"
},
"pytest-clarity": {
"hashes": [
"sha256:5cc99e3d9b7969dfe17e5f6072d45a917c59d363b679686d3c958a1ded2e4dcf"
],
"index": "pypi",
"version": "==0.3.0a0"
}, },
"regex": { "regex": {
"hashes": [ "hashes": [
"sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", "sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7",
"sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", "sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7",
"sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", "sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96",
"sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", "sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1",
"sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", "sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69",
"sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", "sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910",
"sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", "sha256:7faf534c1841c09d8fefa60ccde7b9903c9b528853ecf41628689793290ca143",
"sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", "sha256:b4e0406d822aa4993ac45072a584d57aa4931cf8288b5455bbf30c1d59dbad59",
"sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", "sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2",
"sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", "sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66",
"sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", "sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6",
"sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", "sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a",
"sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", "sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74"
"sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5",
"sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2",
"sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce",
"sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab",
"sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e",
"sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70",
"sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc",
"sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0"
], ],
"version": "==2020.2.20" "version": "==2019.11.1"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
], ],
"version": "==1.14.0" "version": "==1.13.0"
},
"termcolor": {
"hashes": [
"sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"
],
"version": "==1.1.0"
}, },
"toml": { "toml": {
"hashes": [ "hashes": [
@ -384,36 +363,35 @@
}, },
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
], ],
"version": "==1.4.1" "version": "==1.4.0"
}, },
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
], ],
"version": "==0.1.8" "version": "==0.1.7"
} }
} }
} }

@ -1,8 +1,5 @@
# DSpace Statistics API [![Build Status](https://travis-ci.org/ilri/dspace-statistics-api.svg?branch=master)](https://travis-ci.org/ilri/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 Statistics API [![Build Status](https://travis-ci.com/ilri/dspace-statistics-api.svg?branch=master)](https://travis-ci.com/ilri/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.duraspace.org/display/DSDOC5x/REST+API), for example, only exposes information about communities, collections, item metadata, and bitstreams. 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 [REST API](https://wiki.duraspace.org/display/DSDOC5x/REST+API), for example, only exposes information about communities, collections, item metadata, 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.duraspace.org/display/DSPACE/Solr). 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.duraspace.org/display/DSPACE/Solr).
@ -12,7 +9,7 @@ If you use the DSpace Statistics API please cite:
## Requirements ## Requirements
- Python 3.6+ - Python 3.5+
- PostgreSQL version 9.5+ (due to [`UPSERT` support](https://wiki.postgresql.org/wiki/UPSERT)) - PostgreSQL version 9.5+ (due to [`UPSERT` support](https://wiki.postgresql.org/wiki/UPSERT))
- DSpace with [Solr usage statistics enabled](https://wiki.duraspace.org/display/DSDOC5x/SOLR+Statistics) (tested with 5.x) - DSpace with [Solr usage statistics enabled](https://wiki.duraspace.org/display/DSDOC5x/SOLR+Statistics) (tested with 5.x)
@ -81,9 +78,9 @@ The API exposes the following endpoints:
- GET `/`return a basic API documentation page. - 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). - 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).
- 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. - 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.
The item id is the *internal* uuid for an item. You can get these from the standard DSpace REST API. The item id is the *internal* id for an item. You can get these from the standard DSpace REST API.
¹ 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. ¹ 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.
@ -94,6 +91,8 @@ The item id is the *internal* uuid for an item. You can get these from the stand
- Use JSON in PostgreSQL - Use JSON in PostgreSQL
- Add top items endpoint, perhaps `/top/items` or `/items/top`? - Add top items endpoint, perhaps `/top/items` or `/items/top`?
- Make community and collection stats available - Make community and collection stats available
- Support [DSpace 6 UUIDs](https://jira.duraspace.org/browse/DS-1782)
- Switch to [Python 3.6+ f-string syntax](https://realpython.com/python-f-strings/)
- Check IDs in database to see if they are deleted... - Check IDs in database to see if they are deleted...
## License ## License

@ -27,10 +27,11 @@ class AllItemsResource:
cursor.execute("SELECT COUNT(id) FROM items") cursor.execute("SELECT COUNT(id) FROM items")
pages = round(cursor.fetchone()[0] / limit) pages = round(cursor.fetchone()[0] / limit)
# get statistics and use limit and offset to page through results # get statistics, ordered by id, and use limit and offset to page through results
cursor.execute( cursor.execute(
"SELECT id, views, downloads FROM items LIMIT %s OFFSET %s", "SELECT id, views, downloads FROM items ORDER BY id ASC LIMIT {} OFFSET {}".format(
[limit, offset], limit, offset
)
) )
# create a list to hold dicts of item stats # create a list to hold dicts of item stats
@ -40,7 +41,7 @@ class AllItemsResource:
for item in cursor: for item in cursor:
statistics.append( statistics.append(
{ {
"id": str(item["id"]), "id": item["id"],
"views": item["views"], "views": item["views"],
"downloads": item["downloads"], "downloads": item["downloads"],
} }
@ -60,30 +61,26 @@ class ItemResource:
def on_get(self, req, resp, item_id): def on_get(self, req, resp, item_id):
"""Handles GET requests""" """Handles GET requests"""
import psycopg2.extras
# Adapt Pythons uuid.UUID type to PostgreSQLs uuid
# See: https://www.psycopg.org/docs/extras.html
psycopg2.extras.register_uuid()
with DatabaseManager() as db: with DatabaseManager() as db:
db.set_session(readonly=True) db.set_session(readonly=True)
with db.cursor() as cursor: with db.cursor() as cursor:
cursor = db.cursor() cursor = db.cursor()
cursor.execute( cursor.execute(
"SELECT views, downloads FROM items WHERE id=%s", [str(item_id)] "SELECT views, downloads FROM items WHERE id={}".format(item_id)
) )
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise falcon.HTTPNotFound( raise falcon.HTTPNotFound(
title="Item not found", title="Item not found",
description=f'The item with id "{str(item_id)}" was not found.', description='The item with id "{}" was not found.'.format(
item_id
),
) )
else: else:
results = cursor.fetchone() results = cursor.fetchone()
statistics = { statistics = {
"id": str(item_id), "id": item_id,
"views": results["views"], "views": results["views"],
"downloads": results["downloads"], "downloads": results["downloads"],
} }
@ -94,6 +91,6 @@ class ItemResource:
api = application = falcon.API() api = application = falcon.API()
api.add_route("/", RootResource()) api.add_route("/", RootResource())
api.add_route("/items", AllItemsResource()) api.add_route("/items", AllItemsResource())
api.add_route("/item/{item_id:uuid}", ItemResource()) api.add_route("/item/{item_id:int}", ItemResource())
# vim: set sw=4 ts=4 expandtab: # vim: set sw=4 ts=4 expandtab:

@ -15,7 +15,9 @@ class DatabaseManager:
"""Manage database connection.""" """Manage database connection."""
def __init__(self): def __init__(self):
self._connection_uri = f"dbname={DATABASE_NAME} user={DATABASE_USER} password={DATABASE_PASS} host={DATABASE_HOST} port={DATABASE_PORT}" self._connection_uri = "dbname={} user={} password={} host={} port={}".format(
DATABASE_NAME, DATABASE_USER, DATABASE_PASS, DATABASE_HOST, DATABASE_PORT
)
def __enter__(self): def __enter__(self):
try: try:

@ -10,10 +10,10 @@
<ul> <ul>
<li>GET <code>/</code>return a basic API documentation page.</li> <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>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>GET <code>/item/id</code>return views and downloads for a single item (<code>id</code> must be a UUID). Returns HTTP 404 if an item id is not found.</li> <li>GET <code>/item/id</code>return views and downloads for a single item (<code>id</code> must be a positive integer). Returns HTTP 404 if an item id is not found.</li>
</ul> </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> <p>The item id is the <em>internal</em> id for an item. You can get these from the standard DSpace REST API.</p>
<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.</code> <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.</code>
</body> </body>

@ -70,13 +70,13 @@ def get_statistics_shards():
if len(statistics_core_years) > 0: if len(statistics_core_years) > 0:
# Begin building a string of shards starting with the default one # Begin building a string of shards starting with the default one
shards = f"{SOLR_SERVER}/statistics" shards = "{}/statistics".format(SOLR_SERVER)
for core in statistics_core_years: for core in statistics_core_years:
# Create a comma-separated list of shards to pass to our Solr query # Create a comma-separated list of shards to pass to our Solr query
# #
# See: https://wiki.apache.org/solr/DistributedSearch # See: https://wiki.apache.org/solr/DistributedSearch
shards += f",{SOLR_SERVER}/{core}" shards += ",{}/{}".format(SOLR_SERVER, core)
# Return the string of shards, which may actually be empty. Solr doesn't # 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 # seem to mind if the shards query parameter is empty and I haven't seen
@ -134,7 +134,9 @@ def index_views():
while results_current_page <= results_num_pages: while results_current_page <= results_num_pages:
# "pages" are zero based, but one based is more human readable # "pages" are zero based, but one based is more human readable
print( print(
f"Indexing item views (page {results_current_page + 1} of {results_num_pages + 1})" "Indexing item views (page {} of {})".format(
results_current_page + 1, results_num_pages + 1
)
) )
solr_query_params = { solr_query_params = {
@ -217,7 +219,9 @@ def index_downloads():
while results_current_page <= results_num_pages: while results_current_page <= results_num_pages:
# "pages" are zero based, but one based is more human readable # "pages" are zero based, but one based is more human readable
print( print(
f"Indexing item downloads (page {results_current_page + 1} of {results_num_pages + 1})" "Indexing item downloads (page {} of {})".format(
results_current_page + 1, results_num_pages + 1
)
) )
solr_query_params = { solr_query_params = {
@ -260,7 +264,7 @@ with DatabaseManager() as db:
# create table to store item views and downloads # create table to store item views and downloads
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS items """CREATE TABLE IF NOT EXISTS items
(id UUID PRIMARY KEY, views INT DEFAULT 0, downloads INT DEFAULT 0)""" (id INT PRIMARY KEY, views INT DEFAULT 0, downloads INT DEFAULT 0)"""
) )
# commit the table creation before closing the database connection # commit the table creation before closing the database connection

@ -1,37 +1,27 @@
-i https://pypi.org/simple -i https://pypi.org/simple
appdirs==1.4.3
attrs==19.3.0 attrs==19.3.0
backcall==0.1.0 backcall==0.1.0
black==19.10b0 decorator==4.4.1
click==7.0
decorator==4.4.2
entrypoints==0.3 entrypoints==0.3
flake8==3.7.9 flake8==3.7.9
ipython-genutils==0.2.0 ipython-genutils==0.2.0
ipython==7.13.0 ipython==7.9.0
isort==4.3.21 jedi==0.15.1
jedi==0.16.0
mccabe==0.6.1 mccabe==0.6.1
more-itertools==8.2.0 more-itertools==7.2.0
packaging==20.1 packaging==19.2
parso==0.6.2 parso==0.5.1
pathspec==0.7.0 pexpect==4.7.0 ; sys_platform != 'win32'
pexpect==4.8.0 ; sys_platform != 'win32'
pickleshare==0.7.5 pickleshare==0.7.5
pluggy==0.13.1 pluggy==0.13.1
prompt-toolkit==3.0.3 prompt-toolkit==2.0.10
ptyprocess==0.6.0 ptyprocess==0.6.0
py==1.8.1 py==1.8.0
pycodestyle==2.5.0 pycodestyle==2.5.0
pyflakes==2.1.1 pyflakes==2.1.1
pygments==2.5.2 pygments==2.5.1
pyparsing==2.4.6 pyparsing==2.4.5
pytest-clarity==0.3.0a0 pytest==5.3.1
pytest==5.3.5 six==1.13.0
regex==2020.2.20
six==1.14.0
termcolor==1.1.0
toml==0.10.0
traitlets==4.3.3 traitlets==4.3.3
typed-ast==1.4.1 wcwidth==0.1.7
wcwidth==0.1.8

@ -1,9 +1,9 @@
-i https://pypi.org/simple -i https://pypi.org/simple
certifi==2019.11.28 certifi==2019.9.11
chardet==3.0.4 chardet==3.0.4
falcon==2.0.0 falcon==2.0.0
gunicorn==20.0.4 gunicorn==20.0.4
idna==2.9 idna==2.8
psycopg2-binary==2.8.4 psycopg2-binary==2.8.4
requests==2.23.0 requests==2.22.0
urllib3==1.25.8 urllib3==1.25.7

File diff suppressed because it is too large Load Diff

@ -11,57 +11,57 @@ def client():
def test_get_docs(client): def test_get_docs(client):
"""Test requesting the documentation at the root.""" '''Test requesting the documentation at the root.'''
response = client.simulate_get("/") response = client.simulate_get('/')
assert isinstance(response.content, bytes) assert isinstance(response.content, bytes)
assert response.status_code == 200 assert response.status_code == 200
def test_get_item(client): def test_get_item(client):
"""Test requesting a single item.""" '''Test requesting a single item.'''
response = client.simulate_get("/item/c3910974-c3a5-4053-9dce-104aa7bb1621") response = client.simulate_get('/item/17')
response_doc = json.loads(response.text) response_doc = json.loads(response.text)
assert isinstance(response_doc["downloads"], int) assert isinstance(response_doc['downloads'], int)
assert isinstance(response_doc["id"], str) assert isinstance(response_doc['id'], int)
assert isinstance(response_doc["views"], int) assert isinstance(response_doc['views'], int)
assert response.status_code == 200 assert response.status_code == 200
def test_get_missing_item(client): def test_get_missing_item(client):
"""Test requesting a single non-existing item.""" '''Test requesting a single non-existing item.'''
response = client.simulate_get("/item/c3910974-c3a5-4053-9dce-104aa7bb1620") response = client.simulate_get('/item/1')
assert response.status_code == 404 assert response.status_code == 404
def test_get_items(client): def test_get_items(client):
"""Test requesting 100 items.""" '''Test requesting 100 items.'''
response = client.simulate_get("/items", query_string="limit=100") response = client.simulate_get('/items', query_string='limit=100')
response_doc = json.loads(response.text) response_doc = json.loads(response.text)
assert isinstance(response_doc["currentPage"], int) assert isinstance(response_doc['currentPage'], int)
assert isinstance(response_doc["totalPages"], int) assert isinstance(response_doc['totalPages'], int)
assert isinstance(response_doc["statistics"], list) assert isinstance(response_doc['statistics'], list)
assert response.status_code == 200 assert response.status_code == 200
def test_get_items_invalid_limit(client): def test_get_items_invalid_limit(client):
"""Test requesting 100 items with an invalid limit parameter.""" '''Test requesting 100 items with an invalid limit parameter.'''
response = client.simulate_get("/items", query_string="limit=101") response = client.simulate_get('/items', query_string='limit=101')
assert response.status_code == 400 assert response.status_code == 400
def test_get_items_invalid_page(client): def test_get_items_invalid_page(client):
"""Test requesting 100 items with an invalid page parameter.""" '''Test requesting 100 items with an invalid page parameter.'''
response = client.simulate_get("/items", query_string="page=-1") response = client.simulate_get('/items', query_string='page=-1')
assert response.status_code == 400 assert response.status_code == 400