diff --git a/.gitignore b/.gitignore index 716e25c58a793ef0a1470c3dc924baa6a2479c79..9cf181a38e0d982c8da5a8dfab71c6257c6a1c28 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ final/ .$* # Notebooks -.jupyter/ .pytest_cache/ __pycache__/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 08b06743451c8f037a660d9f265506c45bba9895..647d508a1d1631626df1419087760771a7ca59c2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -583,7 +583,7 @@ release-libs: image: docker.io/python:3.11-alpine only: refs: - - /^release-[0-9]+.*/ + - /^release-.*/ variables: PIPENV_PIPFILE: "./dbrepo-analyse-service/Pipfile" script: @@ -591,8 +591,5 @@ release-libs: - pip install pipenv - pipenv install gunicorn && pipenv install --dev --system --deploy - pip install twine build - - 'sed -i -e "s/__APPVERSION__/${APP_VERSION}/g" ./lib/python/pyproject.toml ./lib/python/setup.py ./lib/python/README.md' - - python -m build --sdist ./lib/python - - python -m build --wheel ./lib/python - - printf "[diskutils]\nindex-servers =\n pypi\n\n[pypi]\nusername = __token__\npassword = ${CI_PIPY_TOKEN}\nrepository = https://upload.pypi.org/legacy/" > .pypirc - - python -m twine upload --config-file .pypirc --verbose --repository pypi ./lib/python/dist/dbrepo-* \ No newline at end of file + - ./lib/python/package.sh + - ./lib/python/release.sh \ No newline at end of file diff --git a/.jupyter/.env b/.jupyter/.env new file mode 100644 index 0000000000000000000000000000000000000000..119dca696afda5f423ce2605cfaa41d8c84666c1 --- /dev/null +++ b/.jupyter/.env @@ -0,0 +1,3 @@ +DBREPO_ENDPOINT=https://test.dbrepo.tuwien.ac.at +DBREPO_USERNAME=foo +DBREPO_PASSWORD=bar diff --git a/.jupyter/.gitignore b/.jupyter/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a5aee567d469985ea55322421c496814c680017f --- /dev/null +++ b/.jupyter/.gitignore @@ -0,0 +1,2 @@ +# environment +venv/ \ No newline at end of file diff --git a/.jupyter/default.ipynb b/.jupyter/default.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..0d51aa7188a956b2256a59c0110f9f7d6357cf3c --- /dev/null +++ b/.jupyter/default.ipynb @@ -0,0 +1,99 @@ +{ + "cells": [ + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-04-10T06:10:08.974108Z", + "start_time": "2024-04-10T06:10:07.365075Z" + } + }, + "cell_type": "code", + "source": [ + "!pip install python-dotenv dbrepo==1.4.2rc10\n", + "import dotenv\n", + "%load_ext dotenv\n", + "%dotenv" + ], + "id": "4eb6c2470f464173", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: python-dotenv in ./venv/lib/python3.11/site-packages (1.0.1)\r\n", + "Requirement already satisfied: dbrepo==1.4.2rc10 in ./venv/lib/python3.11/site-packages (1.4.2rc10)\r\n", + "Requirement already satisfied: requests>=2.31 in ./venv/lib/python3.11/site-packages (from dbrepo==1.4.2rc10) (2.31.0)\r\n", + "Requirement already satisfied: pika in ./venv/lib/python3.11/site-packages (from dbrepo==1.4.2rc10) (1.3.2)\r\n", + "Requirement already satisfied: pydantic in ./venv/lib/python3.11/site-packages (from dbrepo==1.4.2rc10) (2.6.4)\r\n", + "Requirement already satisfied: tuspy in ./venv/lib/python3.11/site-packages (from dbrepo==1.4.2rc10) (1.0.3)\r\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in ./venv/lib/python3.11/site-packages (from requests>=2.31->dbrepo==1.4.2rc10) (3.3.2)\r\n", + "Requirement already satisfied: idna<4,>=2.5 in ./venv/lib/python3.11/site-packages (from requests>=2.31->dbrepo==1.4.2rc10) (3.6)\r\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in ./venv/lib/python3.11/site-packages (from requests>=2.31->dbrepo==1.4.2rc10) (2.2.1)\r\n", + "Requirement already satisfied: certifi>=2017.4.17 in ./venv/lib/python3.11/site-packages (from requests>=2.31->dbrepo==1.4.2rc10) (2024.2.2)\r\n", + "Requirement already satisfied: annotated-types>=0.4.0 in ./venv/lib/python3.11/site-packages (from pydantic->dbrepo==1.4.2rc10) (0.6.0)\r\n", + "Requirement already satisfied: pydantic-core==2.16.3 in ./venv/lib/python3.11/site-packages (from pydantic->dbrepo==1.4.2rc10) (2.16.3)\r\n", + "Requirement already satisfied: typing-extensions>=4.6.1 in ./venv/lib/python3.11/site-packages (from pydantic->dbrepo==1.4.2rc10) (4.11.0)\r\n", + "Requirement already satisfied: tinydb>=3.5.0 in ./venv/lib/python3.11/site-packages (from tuspy->dbrepo==1.4.2rc10) (4.8.0)\r\n", + "Requirement already satisfied: aiohttp>=3.6.2 in ./venv/lib/python3.11/site-packages (from tuspy->dbrepo==1.4.2rc10) (3.9.3)\r\n", + "Requirement already satisfied: aiosignal>=1.1.2 in ./venv/lib/python3.11/site-packages (from aiohttp>=3.6.2->tuspy->dbrepo==1.4.2rc10) (1.3.1)\r\n", + "Requirement already satisfied: attrs>=17.3.0 in ./venv/lib/python3.11/site-packages (from aiohttp>=3.6.2->tuspy->dbrepo==1.4.2rc10) (23.2.0)\r\n", + "Requirement already satisfied: frozenlist>=1.1.1 in ./venv/lib/python3.11/site-packages (from aiohttp>=3.6.2->tuspy->dbrepo==1.4.2rc10) (1.4.1)\r\n", + "Requirement already satisfied: multidict<7.0,>=4.5 in ./venv/lib/python3.11/site-packages (from aiohttp>=3.6.2->tuspy->dbrepo==1.4.2rc10) (6.0.5)\r\n", + "Requirement already satisfied: yarl<2.0,>=1.0 in ./venv/lib/python3.11/site-packages (from aiohttp>=3.6.2->tuspy->dbrepo==1.4.2rc10) (1.9.4)\r\n" + ] + } + ], + "execution_count": 1 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-04-10T06:10:09.860353Z", + "start_time": "2024-04-10T06:10:08.981192Z" + } + }, + "cell_type": "code", + "source": [ + "from dbrepo.RestClient import RestClient\n", + "client = RestClient()\n", + "analysis = client.get_licenses()" + ], + "id": "initial_id", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-04-10 08:10:09,327 root DEBUG method: get\n", + "2024-04-10 08:10:09,328 root DEBUG url: https://test.dbrepo.tuwien.ac.at/api/database/license\n", + "2024-04-10 08:10:09,328 root DEBUG stream: False\n", + "2024-04-10 08:10:09,329 root DEBUG secure: True\n", + "2024-04-10 08:10:09,329 root DEBUG username: foo, password: (hidden)\n" + ] + } + ], + "execution_count": 2 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.jupyter/requirements.txt b/.jupyter/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..c06c064500982d0b62a37283aeaac06c85ddb91b --- /dev/null +++ b/.jupyter/requirements.txt @@ -0,0 +1,3 @@ +python-dotenv==1.0.1 +notebook==7.1.2 +dbrepo==1.4.2rc10 diff --git a/bin/test.sh b/bin/test.sh deleted file mode 100755 index 765ecaf6b7e2741a5e18d33f29071309d053de16..0000000000000000000000000000000000000000 --- a/bin/test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -mvn -f ./dbrepo-metadata-service/pom.xml clean install -DskipTests -# test java services -mvn -f ./dbrepo-metadata-service/pom.xml clean test verify -mvn -f ./dbrepo-data-service/pom.xml clean test verify -# test python services -bash ./dbrepo-analyse-service/test.sh -bash ./dbrepo-search-service/test.sh -# test ui -yarn --cwd ./dbrepo-ui install -yarn --cwd ./dbrepo-ui run test:unit -yarn --cwd ./dbrepo-ui run coverage \ No newline at end of file diff --git a/lib/python/Pipfile b/lib/python/Pipfile index a6f3abd4b319a9e9aff95ddc2f3194106eff11b5..b52daf9ec43934af8590c55697672ae74cb1c2f2 100644 --- a/lib/python/Pipfile +++ b/lib/python/Pipfile @@ -8,6 +8,7 @@ requests = "~=2.31" pika = "*" pydantic = "*" tuspy = "*" +pandas = "*" [dev-packages] build = "*" diff --git a/lib/python/Pipfile.lock b/lib/python/Pipfile.lock index d1c478e1745a28f92bc20c94d2b145ad79b85bb2..554a33747d2a6ae6e9e80a30b90228bdf4d98ad2 100644 --- a/lib/python/Pipfile.lock +++ b/lib/python/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8cc220d86727c0bc277f084e48a33c5a487e40639ac8ac36ef8f5d824784261d" + "sha256": "075c662404a333e1251df1222db0602a779feb803e123e80df47260cb2e4ebf9" }, "pipfile-spec": 6, "requires": { @@ -413,6 +413,84 @@ "markers": "python_version >= '3.7'", "version": "==6.0.5" }, + "numpy": { + "hashes": [ + "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", + "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", + "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", + "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", + "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", + "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", + "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", + "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", + "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", + "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", + "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", + "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", + "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", + "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", + "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", + "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", + "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", + "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", + "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", + "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", + "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", + "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", + "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", + "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", + "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", + "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", + "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", + "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", + "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", + "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", + "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", + "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", + "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", + "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", + "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", + "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" + ], + "markers": "python_version == '3.11'", + "version": "==1.26.4" + }, + "pandas": { + "hashes": [ + "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee", + "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e", + "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572", + "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944", + "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403", + "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89", + "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab", + "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6", + "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb", + "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9", + "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019", + "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be", + "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd", + "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c", + "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88", + "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0", + "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397", + "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc", + "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2", + "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7", + "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06", + "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51", + "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0", + "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a", + "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16", + "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02", + "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359", + "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b", + "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.2.1" + }, "pika": { "hashes": [ "sha256:0779a7c1fafd805672796085560d290213a465e4f6f76a6fb19e378d8041a14f", @@ -516,6 +594,21 @@ "markers": "python_version >= '3.8'", "version": "==2.16.3" }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "pytz": { + "hashes": [ + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + ], + "version": "==2024.1" + }, "requests": { "hashes": [ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", @@ -525,6 +618,14 @@ "markers": "python_version >= '3.7'", "version": "==2.31.0" }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, "tinydb": { "hashes": [ "sha256:30c06d12383d7c332e404ca6a6103fb2b32cbf25712689648c39d9a6bd34bd3d", @@ -544,11 +645,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "markers": "python_version >= '3.8'", - "version": "==4.10.0" + "version": "==4.11.0" + }, + "tzdata": { + "hashes": [ + "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", + "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + ], + "markers": "python_version >= '2'", + "version": "==2024.1" }, "urllib3": { "hashes": [ @@ -672,6 +781,14 @@ "markers": "python_version >= '3.7'", "version": "==2.14.0" }, + "backports.tarfile": { + "hashes": [ + "sha256:2688f159c21afd56a07b75f01306f9f52c79aebcc5f4a117fb8fbb4445352c75", + "sha256:bcd36290d9684beb524d3fe74f4a2db056824c47746583f090b8e55daf0776e4" + ], + "markers": "python_version < '3.12'", + "version": "==1.0.0" + }, "beautifulsoup4": { "hashes": [ "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", @@ -682,12 +799,12 @@ }, "build": { "hashes": [ - "sha256:8ed0851ee76e6e38adce47e4bee3b51c771d86c64cf578d0c2245567ee200e73", - "sha256:8eea65bb45b1aac2e734ba2cc8dad3a6d97d97901a395bd0ed3e7b46953d2a31" + "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d", + "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.1.1" + "markers": "python_version >= '3.8'", + "version": "==1.2.1" }, "certifi": { "hashes": [ @@ -999,19 +1116,19 @@ }, "jaraco.classes": { "hashes": [ - "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206", - "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30" + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" ], "markers": "python_version >= '3.8'", - "version": "==3.3.1" + "version": "==3.4.0" }, "jaraco.context": { "hashes": [ - "sha256:4dad2404540b936a20acedec53355bdaea223acb88fd329fa6de9261c941566e", - "sha256:5d9e95ca0faa78943ed66f6bc658dd637430f16125d86988e77844c741ff2f11" + "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266", + "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2" ], - "markers": "python_version >= '3.7'", - "version": "==4.3.0" + "markers": "python_version >= '3.8'", + "version": "==5.3.0" }, "jaraco.functools": { "hashes": [ @@ -1039,11 +1156,11 @@ }, "keyring": { "hashes": [ - "sha256:9a15cd280338920388e8c1787cb8792b9755dabb3e7c61af5ac1f8cd437cefde", - "sha256:fc024ed53c7ea090e30723e6bd82f58a39dc25d9a6797d866203ecd0ee6306cb" + "sha256:26fc12e6a329d61d24aa47b22a7c5c3f35753df7d8f2860973cf94f4e1fb3427", + "sha256:7230ea690525133f6ad536a9b5def74a4bd52642abe594761028fc044d7c7893" ], "markers": "python_version >= '3.8'", - "version": "==25.0.0" + "version": "==25.1.0" }, "markdown-it-py": { "hashes": [ @@ -1137,24 +1254,24 @@ }, "nh3": { "hashes": [ - "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770", - "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf", - "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305", - "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601", - "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28", - "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7", - "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3", - "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911", - "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf", - "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0", - "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5", - "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97", - "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d", - "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e", - "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3", - "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6" - ], - "version": "==0.2.15" + "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a", + "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911", + "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb", + "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a", + "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc", + "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028", + "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9", + "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3", + "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351", + "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10", + "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71", + "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f", + "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b", + "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a", + "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062", + "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a" + ], + "version": "==0.2.17" }, "packaging": { "hashes": [ @@ -1182,10 +1299,11 @@ }, "pycparser": { "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" ], - "version": "==2.21" + "markers": "python_version >= '3.8'", + "version": "==2.22" }, "pygments": { "hashes": [ @@ -1231,11 +1349,12 @@ }, "requests-mock": { "hashes": [ - "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4", - "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15" + "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", + "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401" ], "index": "pypi", - "version": "==1.11.0" + "markers": "python_version >= '3.5'", + "version": "==1.12.1" }, "requests-toolbelt": { "hashes": [ @@ -1278,14 +1397,6 @@ "markers": "python_version >= '3.8'", "version": "==69.2.0" }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, "snowballstemmer": { "hashes": [ "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", diff --git a/lib/python/README.md b/lib/python/README.md index 0cb742a451c6b6e9cb8e44bdc3b5068fd3d5d2b3..610c6eded42360330561c723ef1283d075ec6b2c 100644 --- a/lib/python/README.md +++ b/lib/python/README.md @@ -32,33 +32,33 @@ print(f"Analysis result: {analysis}") # create table table = client.create_table(database_id=1, - name="Sensor Data", - constraints=CreateTableConstraints( - checks=['precipitation >= 0'], - uniques=[['precipitation']]), - columns=[CreateTableColumn(name="date", - type=ColumnType.DATE, - dfid=3, # YYYY-MM-dd - primary_key=True, - null_allowed=False), - CreateTableColumn(name="precipitation", - type=ColumnType.DECIMAL, - size=10, - d=4, - primary_key=False, - null_allowed=True), - CreateTableColumn(name="lat", - type=ColumnType.DECIMAL, - size=10, - d=4, - primary_key=False, - null_allowed=True), - CreateTableColumn(name="lng", - type=ColumnType.DECIMAL, - size=10, - d=4, - primary_key=False, - null_allowed=True)]) + name="Sensor Data", + constraints=CreateTableConstraints( + checks=['precipitation >= 0'], + uniques=[['precipitation']]), + columns=[CreateTableColumn(name="date", + type=ColumnType.DATE, + dfid=3, # YYYY-MM-dd + primary_key=True, + null_allowed=False), + CreateTableColumn(name="precipitation", + type=ColumnType.DECIMAL, + size=10, + d=4, + primary_key=False, + null_allowed=True), + CreateTableColumn(name="lat", + type=ColumnType.DECIMAL, + size=10, + d=4, + primary_key=False, + null_allowed=True), + CreateTableColumn(name="lng", + type=ColumnType.DECIMAL, + size=10, + d=4, + primary_key=False, + null_allowed=True)]) print(f"Create table result {table}") # -> (id=1, internal_name=sensor_data, ...) @@ -83,7 +83,25 @@ print(f"Finished.") queries ([docs](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//usage-overview/#export-subset)) - Get data from tables/views/subsets -## Future +## Configure + +All credentials can optionally be set/overridden with environment variables. This is especially useful when sharing +Jupyter Notebooks by creating an invisible `.env` file and loading it: + +``` +REST_API_ENDPOINT="https://test.dbrepo.tuwien.ac.at" +REST_API_USERNAME="foo" +REST_API_PASSWORD="bar" +REST_API_SECURE="True" +AMQP_API_HOST="https://test.dbrepo.tuwien.ac.at" +AMQP_API_PORT="5672" +AMQP_API_USERNAME="foo" +AMQP_API_PASSWORD="bar" +AMQP_API_VIRTUAL_HOST="/" +REST_UPLOAD_ENDPOINT="https://test.dbrepo.tuwien.ac.at/api/upload/files" +``` + +## Roadmap - Searching diff --git a/lib/python/build.sh b/lib/python/build.sh deleted file mode 100644 index 802067b6beb9c11b6f34f703a2f1a8257fba5d3a..0000000000000000000000000000000000000000 --- a/lib/python/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# needed for MariaDB Connector/C -apt update && apt install -y curl gcc libmariadb3 libmariadb-dev - -python3 -m venv ./lib/python/venv -source ./lib/python/venv/bin/activate -PIPENV_PIPFILE=./lib/python/Pipfile pipenv install --dev \ No newline at end of file diff --git a/lib/python/dbrepo/AmqpClient.py b/lib/python/dbrepo/AmqpClient.py index c9fcdc05ffe9116083c42c24b73242c80c43e431..29f7e261ec638eff1f7d2287219f4067ff30af65 100644 --- a/lib/python/dbrepo/AmqpClient.py +++ b/lib/python/dbrepo/AmqpClient.py @@ -2,8 +2,8 @@ import dataclasses import os import pika import sys -import logging import json +import logging from dbrepo.api.dto import CreateData @@ -32,16 +32,18 @@ class AmqpClient: broker_virtual_host: str = '/', username: str = None, password: str = None) -> None: + logging.getLogger('requests').setLevel(logging.INFO) + logging.getLogger('urllib3').setLevel(logging.INFO) logging.basicConfig(format='%(asctime)s %(name)-12s %(levelname)-6s %(message)s', level=logging.DEBUG, stream=sys.stdout) - self.broker_host = os.environ.get('DBREPO_BROKER_HOST', broker_host) - self.broker_port = os.environ.get('DBREPO_BROKER_PORT', broker_port) - if os.environ.get('DBREPO_BROKER_VIRTUAL_HOST') is not None: - self.broker_virtual_host = os.environ.get('DBREPO_BROKER_VIRTUAL_HOST') + self.broker_host = os.environ.get('AMQP_API_HOST', broker_host) + self.broker_port = os.environ.get('AMQP_API_PORT', broker_port) + if os.environ.get('AMQP_API_VIRTUAL_HOST') is not None: + self.broker_virtual_host = os.environ.get('AMQP_API_VIRTUAL_HOST') else: self.broker_virtual_host = broker_virtual_host - self.username = os.environ.get('DBREPO_USERNAME', username) - self.password = os.environ.get('DBREPO_PASSWORD', password) + self.username = os.environ.get('AMQP_API_USERNAME', username) + self.password = os.environ.get('AMQP_API_PASSWORD', password) def publish(self, exchange: str, routing_key: str, data=dict) -> None: """ diff --git a/lib/python/dbrepo/RestClient.py b/lib/python/dbrepo/RestClient.py index a114dcc9889fc7fa81de30ff716082a905d2c4eb..5aad7d6eb3c16ce79811d6a66773a0087e69cc1c 100644 --- a/lib/python/dbrepo/RestClient.py +++ b/lib/python/dbrepo/RestClient.py @@ -5,7 +5,9 @@ import logging import requests from pydantic import TypeAdapter from tusclient.client import TusClient +from pandas import DataFrame +from dbrepo.UploadClient import UploadClient from dbrepo.api.dto import * from dbrepo.api.exceptions import ResponseCodeError, UsernameExistsError, EmailExistsError, NotExistsError, \ ForbiddenError, MalformedError, NameExistsError, QueryStoreError, MetadataConsistencyError, ExternalSystemError, \ @@ -15,8 +17,8 @@ from dbrepo.api.exceptions import ResponseCodeError, UsernameExistsError, EmailE class RestClient: """ The RestClient class for communicating with the DBRepo REST API. All parameters can be set also via environment \ - variables, e.g. set endpoint with DBREPO_ENDPOINT, username with DBREPO_USERNAME, etc. You can override the \ - constructor parameters with the environment variables. + variables, e.g. set endpoint with REST_API_ENDPOINT, username with REST_API_USERNAME, etc. You can override \ + the constructor parameters with the environment variables. :param endpoint: The REST API endpoint. Optional. Default: "http://gateway-service" :param username: The REST API username. Optional. @@ -38,11 +40,11 @@ class RestClient: logging.getLogger('urllib3').setLevel(logging.INFO) logging.basicConfig(format='%(asctime)s %(name)-12s %(levelname)-6s %(message)s', level=logging.DEBUG, stream=sys.stdout) - self.endpoint = os.environ.get('DBREPO_ENDPOINT', endpoint) - self.username = os.environ.get('DBREPO_USERNAME', username) - self.password = os.environ.get('DBREPO_PASSWORD', password) - if os.environ.get('DBREPO_SECURE') is not None: - self.secure = os.environ.get('DBREPO_SECURE') == 'True' + self.endpoint = os.environ.get('REST_API_ENDPOINT', endpoint) + self.username = os.environ.get('REST_API_USERNAME', username) + self.password = os.environ.get('REST_API_PASSWORD', password) + if os.environ.get('REST_API_SECURE') is not None: + self.secure = os.environ.get('REST_API_SECURE') == 'True' else: self.secure = secure @@ -86,9 +88,11 @@ class RestClient: raise UploadError(f'Failed to upload the file to {self.endpoint}') return filename - def whoami(self) -> str: + def whoami(self) -> str | None: """ Print the username. + + :returns: The username, if set. """ if self.username is not None: logging.info(f"{self.username}") @@ -124,7 +128,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return User.parse_raw(body) + return User.model_validate(body) if response.status_code == 404: raise NotExistsError(f'Failed to find user with id {user_id}') raise ResponseCodeError( @@ -151,7 +155,7 @@ class RestClient: payload=CreateUser(username=username, password=password, email=email)) if response.status_code == 201: body = response.json() - return UserBrief.parse_raw(body) + return UserBrief.model_validate(body) if response.status_code == 403: raise ForbiddenError(f'Failed to update user password: not allowed') if response.status_code == 404: @@ -186,7 +190,7 @@ class RestClient: orcid=orcid)) if response.status_code == 202: body = response.json() - return User.parse_raw(body) + return User.model_validate(body) if response.status_code == 400: raise ResponseCodeError(f'Failed to update user: invalid values') if response.status_code == 403: @@ -215,7 +219,7 @@ class RestClient: response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateUserTheme(theme=theme)) if response.status_code == 202: body = response.json() - return User.parse_raw(body) + return User.model_validate(body) if response.status_code == 400: raise ResponseCodeError(f'Failed to update user theme: invalid values') if response.status_code == 403: @@ -244,7 +248,7 @@ class RestClient: response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateUserPassword(password=password)) if response.status_code == 202: body = response.json() - return User.parse_raw(body) + return User.model_validate(body) if response.status_code == 400: raise ResponseCodeError(f'Failed to update user password: invalid values') if response.status_code == 403: @@ -286,7 +290,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return Container.parse_raw(body) + return Container.model_validate(body) if response.status_code == 404: raise NotExistsError(f'Failed to get container: not found') raise ResponseCodeError(f'Failed to get container: response code: {response.status_code} is not 200 (OK)') @@ -333,7 +337,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return Database.parse_raw(body) + return Database.model_validate(body) if response.status_code == 404: raise NotExistsError(f'Failed to find database with id {database_id}') raise ResponseCodeError( @@ -359,7 +363,7 @@ class RestClient: payload=CreateDatabase(name=name, container_id=container_id, is_public=is_public)) if response.status_code == 201: body = response.json() - return Database.parse_raw(body) + return Database.model_validate(body) if response.status_code == 403: raise ForbiddenError(f'Failed to create database: not allowed') if response.status_code == 404: @@ -385,7 +389,7 @@ class RestClient: response = self._wrapper(method="put", url=url, force_auth=True, payload=ModifyVisibility(is_public=is_public)) if response.status_code == 202: body = response.json() - return Database.parse_raw(body) + return Database.model_validate(body) if response.status_code == 403: raise ForbiddenError(f'Failed to update database visibility: not allowed') if response.status_code == 404: @@ -409,7 +413,7 @@ class RestClient: response = self._wrapper(method="put", url=url, force_auth=True, payload=ModifyOwner(id=user_id)) if response.status_code == 202: body = response.json() - return Database.parse_raw(body) + return Database.model_validate(body) if response.status_code == 403: raise ForbiddenError(f'Failed to update database visibility: not allowed') if response.status_code == 404: @@ -442,7 +446,7 @@ class RestClient: columns=columns, constraints=constraints)) if response.status_code == 201: body = response.json() - return Table.parse_raw(body) + return Table.model_validate(body) if response.status_code == 400: raise MalformedError(f'Failed to create table: service rejected malformed payload') if response.status_code == 403: @@ -488,7 +492,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return Table.parse_raw(body) + return Table.model_validate(body) if response.status_code == 403: raise ForbiddenError(f'Failed to find table: not allowed') if response.status_code == 404: @@ -556,7 +560,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return View.parse_raw(body) + return View.model_validate(body) if response.status_code == 403: raise ForbiddenError(f'Failed to find view: not allowed') if response.status_code == 404: @@ -584,7 +588,7 @@ class RestClient: payload=CreateView(name=name, query=query, is_public=is_public)) if response.status_code == 201: body = response.json() - return View.parse_raw(body) + return View.model_validate(body) if response.status_code == 400 or response.status_code == 423: raise MalformedError(f'Failed to create view: service rejected malformed payload') if response.status_code == 403 or response.status_code == 405: @@ -616,7 +620,8 @@ class RestClient: raise NotExistsError(f'Failed to delete view: not found') raise ResponseCodeError(f'Failed to delete view: response code: {response.status_code} is not 202 (ACCEPTED)') - def get_view_data(self, database_id: int, view_id: int, page: int = 0, size: int = 10) -> Result: + def get_view_data(self, database_id: int, view_id: int, page: int = 0, size: int = 10, + df: bool = False) -> Result | DataFrame: """ Get data of a view in a database with given database id and view id. @@ -624,6 +629,7 @@ class RestClient: :param view_id: The view id. :param page: The result pagination number. Optional. Default: 0. :param size: The result pagination size. Optional. Default: 10. + :param df: If true, the result is returned as Pandas DataFrame. Optional. Default: False. :returns: The result of the view query, if successful. @@ -639,7 +645,10 @@ class RestClient: response = self._wrapper(method="get", url=url, params=params) if response.status_code == 200: body = response.json() - return Result.parse_raw(body) + res = Result.model_validate(body) + if df: + return DataFrame.from_records(res.result) + return res if response.status_code == 400: raise MalformedError(f'Failed to get view data: service rejected malformed payload') if response.status_code == 403: @@ -649,7 +658,7 @@ class RestClient: raise ResponseCodeError(f'Failed to get view data: response code: {response.status_code} is not 200 (OK)') def get_table_data(self, database_id: int, table_id: int, page: int = 0, size: int = 10, - timestamp: datetime.datetime = None) -> Result: + timestamp: datetime.datetime = None, df: bool = False) -> Result | DataFrame: """ Get data of a table in a database with given database id and table id. @@ -658,6 +667,7 @@ class RestClient: :param page: The result pagination number. Optional. Default: 0. :param size: The result pagination size. Optional. Default: 10. :param timestamp: The query execution time. Optional. + :param df: If true, the result is returned as Pandas DataFrame. Optional. Default: False. :returns: The result of the view query, if successful. @@ -676,7 +686,10 @@ class RestClient: response = self._wrapper(method="get", url=url, params=params) if response.status_code == 200: body = response.json() - return Result.parse_raw(body) + res = Result.model_validate(body) + if df: + return DataFrame.from_records(res.result) + return res if response.status_code == 400: raise MalformedError(f'Failed to get table data: service rejected malformed payload') if response.status_code == 403: @@ -736,8 +749,8 @@ class RestClient: :raises NotExistsError: If the table does not exist. :raises MalformedError: If the payload is rejected by the service (e.g. LOB data could not be imported). """ - upload = UploadClient(endpoint=self.endpoint) - filename = upload.upload(file_path=file_path) + client = UploadClient(endpoint=self.endpoint) + filename = client.upload(file_path=file_path) url = f'/api/database/{database_id}/table/{table_id}/data/import' response = self._wrapper(method="post", url=url, force_auth=True, payload=Import(location=filename, separator=separator, quote=quote, @@ -790,7 +803,7 @@ class RestClient: response = self._wrapper(method="get", url=url, params=params) if response.status_code == 202: body = response.json() - return DatatypeAnalysis.parse_raw(body) + return DatatypeAnalysis.model_validate(body) if response.status_code == 400 or response.status_code == 500: raise MalformedError(f'Failed to analyse data types: service rejected malformed payload') if response.status_code == 404: @@ -826,7 +839,7 @@ class RestClient: response = self._wrapper(method="get", url=url, params=params) if response.status_code == 202: body = response.json() - return KeyAnalysis.parse_raw(body) + return KeyAnalysis.model_validate(body) if response.status_code == 400 or response.status_code == 500: raise MalformedError(f'Failed to analyse data types: service rejected malformed payload') if response.status_code == 404: @@ -851,7 +864,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 202: body = response.json() - return TableStatistics.parse_raw(body) + return TableStatistics.model_validate(body) if response.status_code == 400: raise MalformedError(f'Failed to analyse table statistics: service rejected malformed payload') if response.status_code == 404: @@ -994,7 +1007,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return DatabaseAccess.parse_raw(body).type + return DatabaseAccess.model_validate(body).type if response.status_code == 403: raise ForbiddenError(f'Failed to get database access: not allowed') if response.status_code == 404: @@ -1020,7 +1033,7 @@ class RestClient: response = self._wrapper(method="post", url=url, force_auth=True, payload=CreateAccess(type=type)) if response.status_code == 202: body = response.json() - return DatabaseAccess.parse_raw(body).type + return DatabaseAccess.model_validate(body).type if response.status_code == 400: raise MalformedError(f'Failed to create database access: service rejected malformed payload') if response.status_code == 403 or response.status_code == 405: @@ -1049,7 +1062,7 @@ class RestClient: response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateAccess(type=type)) if response.status_code == 202: body = response.json() - return DatabaseAccess.parse_raw(body).type + return DatabaseAccess.model_validate(body).type if response.status_code == 400: raise MalformedError(f'Failed to update database access: service rejected malformed payload') if response.status_code == 403 or response.status_code == 405: @@ -1113,7 +1126,7 @@ class RestClient: payload=ExecuteQuery(statement=query, timestamp=timestamp)) if response.status_code == 202: body = response.json() - return Result.parse_raw(body) + return Result.model_validate(body) if response.status_code == 400: raise MalformedError(f'Failed to execute query: service rejected malformed payload') if response.status_code == 403: @@ -1128,7 +1141,7 @@ class RestClient: f'Failed to execute query: response code: {response.status_code} is not 202 (ACCEPTED)') def get_query_data(self, database_id: int, query_id: int, page: int = 0, size: int = 10, - file_path: str = None) -> Result: + df: bool = False) -> Result | DataFrame: """ Re-executes a query in a database with given database id and query id. @@ -1137,7 +1150,7 @@ class RestClient: :param page: The result pagination number. Optional. Default: 0. :param size: The result pagination size. Optional. Default: 10. :param size: The result pagination size. Optional. Default: 10. - :param file_path: The file path where the result should be saved. Optional. + :param df: If true, the result is returned as Pandas DataFrame. Optional. Default: False. :returns: The result set, if successful. @@ -1148,22 +1161,17 @@ class RestClient: :raises QueryStoreError: The query store rejected the query. :raises MetadataConsistencyError: The service failed to parse columns from the metadata database. """ - stream = False headers = {} url = f'/api/database/{database_id}/query/{query_id}/data' if page is not None and size is not None: url += f'?page={page}&size={size}' - if file_path is not None: - stream = True - headers = {'Accept': 'text/csv'} - response = self._wrapper(method="get", url=url, headers=headers, stream=stream) + response = self._wrapper(method="get", url=url, headers=headers) if response.status_code == 200: - if file_path is None: - body = response.json() - return Result.parse_raw(body) - else: - with open(file_path, "w") as f: - f.write(response.content.decode("utf-8")) + body = response.json() + res = Result.model_validate(body) + if df: + return DataFrame.from_records(res.result) + return res if response.status_code == 400: raise MalformedError(f'Failed to re-execute query: service rejected malformed payload') if response.status_code == 403 or response.status_code == 405: @@ -1233,7 +1241,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return Query.parse_raw(body) + return Query.model_validate(body) if response.status_code == 404: raise NotExistsError(f'Failed to find query: not found') if response.status_code == 403 or response.status_code == 405: @@ -1295,7 +1303,7 @@ class RestClient: response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateQuery(persist=persist)) if response.status_code == 202: body = response.json() - return Query.parse_raw(body) + return Query.model_validate(body) if response.status_code == 403 or response.status_code == 405: raise ForbiddenError(f'Failed to update query: not allowed') if response.status_code == 404: @@ -1349,7 +1357,7 @@ class RestClient: response = self._wrapper(method="post", url=url, force_auth=True, payload=payload) if response.status_code == 201: body = response.json() - return Identifier.parse_raw(body) + return Identifier.model_validate(body) if response.status_code == 400: raise MalformedError(f'Failed to create identifier: service rejected malformed payload') if response.status_code == 403 or response.status_code == 405: @@ -1376,7 +1384,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return Identifier.parse_raw(body) + return Identifier.model_validate(body) if response.status_code == 404: raise NotExistsError(f'Failed to suggest identifier: not found or not supported') raise ResponseCodeError(f'Failed to suggest identifier: response code: {response.status_code} is not 200 (OK)') @@ -1442,7 +1450,7 @@ class RestClient: payload=UpdateColumn(concept_uri=concept_uri, unit_uri=unit_uri)) if response.status_code == 202: body = response.json() - return Column.parse_raw(body) + return Column.model_validate(body) if response.status_code == 400: raise MalformedError(f'Failed to update column: service rejected malformed payload') if response.status_code == 403: diff --git a/lib/python/dbrepo/UploadClient.py b/lib/python/dbrepo/UploadClient.py new file mode 100644 index 0000000000000000000000000000000000000000..236453cb700cb6a1634c9910b66686543fda0aab --- /dev/null +++ b/lib/python/dbrepo/UploadClient.py @@ -0,0 +1,39 @@ +import logging +import os +import re +import sys +from tusclient import client + + +class UploadClient: + """ + The UploadClient class for communicating with the DBRepo REST API. All parameters can be set also via environment \ + variables, e.g. set endpoint with DBREPO_ENDPOINT, username with DBREPO_USERNAME, etc. You can override the \ + constructor parameters with the environment variables. + + :param endpoint: The REST API endpoint. Optional. Default: "http://gateway-service/api/upload/files" + """ + endpoint: str = None + + def __init__(self, endpoint: str = 'http://gateway-service/api/upload/files') -> None: + logging.getLogger('requests').setLevel(logging.INFO) + logging.getLogger('urllib3').setLevel(logging.INFO) + logging.basicConfig(format='%(asctime)s %(name)-12s %(levelname)-6s %(message)s', level=logging.DEBUG, + stream=sys.stdout) + self.endpoint = os.environ.get('REST_UPLOAD_ENDPOINT', endpoint) + + def upload(self, file_path: str) -> str: + """ + Imports a file through the Upload Service into the Storage Service. + + :param file_path: The file path on the local machine. + + :returns: Filename on the Storage Service, if successful. + """ + tus_client = client.TusClient(url=self.endpoint) + uploader = tus_client.uploader(file_path=file_path) + uploader.upload() + m = re.search('\\/([a-f0-9]+)\\+', uploader.url) + filename = m.group(0)[1:-1] + logging.debug(f'uploaded file {file_path} to storage service with key: {filename}') + return filename diff --git a/lib/python/debug.py b/lib/python/debug.py deleted file mode 100644 index afcb58609017fba7c51b6465e2a637f55a97037e..0000000000000000000000000000000000000000 --- a/lib/python/debug.py +++ /dev/null @@ -1,5 +0,0 @@ -from dbrepo.RestClient import RestClient - -client = RestClient(endpoint="https://dbrepo1.ec.tuwien.ac.at", username="foo", - password="bar") -client.get_licenses() diff --git a/lib/python/docs/guide/amqp-client.rst b/lib/python/docs/guide/amqp-client.rst index 0bb970292c2391305c45d3feff0b9791a92f05af..7f87621f43f486dc7692a20920b9ba36894a8338 100644 --- a/lib/python/docs/guide/amqp-client.rst +++ b/lib/python/docs/guide/amqp-client.rst @@ -1,8 +1,5 @@ -AMQP Client -=========== - -.. warning:: - This documentation is a work in progress. +AMQP API Client +=============== .. automodule:: dbrepo.AmqpClient :members: diff --git a/lib/python/docs/guide/rest-client.rst b/lib/python/docs/guide/rest-client.rst index 4af546168d3a4142566967982339dbba4f8f0032..09ed4019436d79c019e6a9f22803d92cc72ed419 100644 --- a/lib/python/docs/guide/rest-client.rst +++ b/lib/python/docs/guide/rest-client.rst @@ -1,8 +1,5 @@ -REST Client -=========== - -.. warning:: - This documentation is a work in progress. +REST API Client +=============== .. automodule:: dbrepo.RestClient :members: diff --git a/lib/python/docs/guide/upload-client.rst b/lib/python/docs/guide/upload-client.rst new file mode 100644 index 0000000000000000000000000000000000000000..4070b0908894914d0391e0ebbffa37d81c499a88 --- /dev/null +++ b/lib/python/docs/guide/upload-client.rst @@ -0,0 +1,6 @@ +Upload API Client +================= + +.. automodule:: dbrepo.UploadClient + :members: + :no-index: diff --git a/lib/python/docs/index.rst b/lib/python/docs/index.rst index e084d4c3b2c07f1b601165b7d1d7f518bf9c8a2d..d1916a2e2aafc6173b2003379a08ae7140b98559 100644 --- a/lib/python/docs/index.rst +++ b/lib/python/docs/index.rst @@ -1,18 +1,28 @@ -DBRepo Python Library -===================== +DBRepo Python Library documentation +=================================== -.. image:: https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/badges/release.svg - :alt: DBRepo latest release version - :target: https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services +Use the DBRepo SDK to create, update, configure and manage DBRepo services such as the Data Service to get data as +Pandas `DataFrame <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html>`_ for analysis. The DBRepo SDK +provides an object-oriented API as well as low-level access to DBRepo services. -.. image:: https://img.shields.io/pypi/dm/dbrepo - :alt: PyPI downloads per month - :target: https://pypi.org/project/dbrepo/__APPVERSION__/ +.. note:: + The SDK has been implemented and documented for DBRepo version 1.4.2, earlier versions are not supported. -.. warning:: - This documentation is a work in progress. +Quickstart +---------- -REST Client +Find numerous quickstart examples on +the `DBRepo website <https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/__APPVERSION__/usage-overview/>`_. + +AMQP API Client +----------- + +.. toctree:: + :maxdepth: 2 + + guide/amqp-client + +REST API Client ----------- .. toctree:: @@ -20,13 +30,13 @@ REST Client guide/rest-client -AMQP Client +Upload API Client ----------- .. toctree:: :maxdepth: 2 - guide/amqp-client + guide/upload-client Indices and tables ================== diff --git a/lib/python/package.sh b/lib/python/package.sh new file mode 100755 index 0000000000000000000000000000000000000000..4478dbf962d7dba47fc787061b5761d2b53a68eb --- /dev/null +++ b/lib/python/package.sh @@ -0,0 +1,8 @@ +#!/bin/bash +PRE_RELEASE="" +if [ "${CI_COMMIT_BRANCH:8:8}" = "master" ]; then + PRE_RELEASE="rc${CI_PIPELINE_ID}" +fi +sed -i -e "s/__APPVERSION__/${APP_VERSION}${PRE_RELEASE}/g" ./lib/python/pyproject.toml ./lib/python/setup.py ./lib/python/README.md +python -m build --sdist ./lib/python +python -m build --wheel ./lib/python diff --git a/lib/python/pyproject.toml b/lib/python/pyproject.toml index 6dfae10925cfb7d47f9d0272d5ab59ddb884bcc1..43baf9a5f11d5ca0f9806173dd148816168bd9d5 100644 --- a/lib/python/pyproject.toml +++ b/lib/python/pyproject.toml @@ -23,7 +23,8 @@ dependencies = [ "requests >= 2.31", "pika", "pydantic", - "tuspy" + "tuspy", + "pandas" ] [build-system] diff --git a/lib/python/release.sh b/lib/python/release.sh new file mode 100755 index 0000000000000000000000000000000000000000..5e2b32683188c63d1c7fe36a80bbdfce6728ff45 --- /dev/null +++ b/lib/python/release.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cat ${CI_PIPYRC} | base64 -d > .pypirc +python -m twine upload --config-file .pypirc --verbose --repository pypi ./lib/python/dist/dbrepo-* diff --git a/lib/python/tests/test_analyse.py b/lib/python/tests/test_analyse.py index c87cb22018ba81634b9e3526e7051688ad31ee97..a4668fedc500dbd8c6ebebf9edb6f3885156f5eb 100644 --- a/lib/python/tests/test_analyse.py +++ b/lib/python/tests/test_analyse.py @@ -13,7 +13,7 @@ class AnalyseTest(unittest.TestCase): with requests_mock.Mocker() as mock: exp = KeyAnalysis(keys={'id': 0, 'firstname': 1, 'lastname': 2}) # mock - mock.get('/api/analyse/keys', json=exp.model_dump_json(), status_code=202) + mock.get('/api/analyse/keys', json=exp.model_dump(), status_code=202) # test response = RestClient().analyse_keys(file_path='f705a7bd0cb2d5e37ab2b425036810a2', separator=',', upload=False) diff --git a/lib/python/tests/test_container.py b/lib/python/tests/test_container.py index d948e337db607a0898f1cfb641a32d10bf821878..e9988f19ca9dd1567d48295c7c4e3184acc30b83 100644 --- a/lib/python/tests/test_container.py +++ b/lib/python/tests/test_container.py @@ -94,7 +94,7 @@ class ContainerTest(unittest.TestCase): ]), hash="f829dd8a884182d0da846f365dee1221fd16610a14c81b8f9f295ff162749e50") # mock - mock.get('/api/container/1', json=exp.model_dump_json()) + mock.get('/api/container/1', json=exp.model_dump()) # test response = RestClient().get_container(container_id=1) self.assertEqual(exp, response) diff --git a/lib/python/tests/test_database.py b/lib/python/tests/test_database.py index 9df3a11d5a3a26795ff98bab60429a12f37ad333..017a17445ab4a7e4eb0e56002f1a444da0208118 100644 --- a/lib/python/tests/test_database.py +++ b/lib/python/tests/test_database.py @@ -110,7 +110,7 @@ class DatabaseTest(unittest.TestCase): ) with requests_mock.Mocker() as mock: # mock - mock.get('/api/database/1', json=exp.model_dump_json()) + mock.get('/api/database/1', json=exp.model_dump()) # test response = RestClient().get_database(1) self.assertEqual(exp, response) @@ -178,7 +178,7 @@ class DatabaseTest(unittest.TestCase): ) with requests_mock.Mocker() as mock: # mock - mock.post('/api/database', json=exp.model_dump_json(), status_code=201) + mock.post('/api/database', json=exp.model_dump(), status_code=201) # test client = RestClient(username="a", password="b") response = client.create_database(name='test', container_id=1, is_public=True) @@ -253,7 +253,7 @@ class DatabaseTest(unittest.TestCase): ) with requests_mock.Mocker() as mock: # mock - mock.put('/api/database/1', json=exp.model_dump_json(), status_code=202) + mock.put('/api/database/1', json=exp.model_dump(), status_code=202) # test client = RestClient(username="a", password="b") response = client.update_database_visibility(database_id=1, is_public=True) @@ -328,7 +328,7 @@ class DatabaseTest(unittest.TestCase): ) with requests_mock.Mocker() as mock: # mock - mock.put('/api/database/1/owner', json=exp.model_dump_json(), status_code=202) + mock.put('/api/database/1/owner', json=exp.model_dump(), status_code=202) # test client = RestClient(username="a", password="b") response = client.update_database_owner(database_id=1, user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') @@ -376,7 +376,7 @@ class DatabaseTest(unittest.TestCase): attributes=UserAttributes(theme='light'))) with requests_mock.Mocker() as mock: # mock - mock.get('/api/database/1/access', json=exp.model_dump_json()) + mock.get('/api/database/1/access', json=exp.model_dump()) # test response = RestClient().get_database_access(database_id=1) self.assertEqual(response, AccessType.READ) @@ -408,7 +408,7 @@ class DatabaseTest(unittest.TestCase): attributes=UserAttributes(theme='light'))) with requests_mock.Mocker() as mock: # mock - mock.post('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', json=exp.model_dump_json(), + mock.post('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', json=exp.model_dump(), status_code=202) # test client = RestClient(username="a", password="b") @@ -470,7 +470,7 @@ class DatabaseTest(unittest.TestCase): attributes=UserAttributes(theme='light'))) with requests_mock.Mocker() as mock: # mock - mock.put('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', json=exp.model_dump_json(), + mock.put('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', json=exp.model_dump(), status_code=202) # test client = RestClient(username="a", password="b") diff --git a/lib/python/tests/test_identifier.py b/lib/python/tests/test_identifier.py index c39e7a99a5a54747da17274a1d89acec3ba9c2eb..64ebfe1f5166dcc1de0f6fb5c3fb28283ce8bf53 100644 --- a/lib/python/tests/test_identifier.py +++ b/lib/python/tests/test_identifier.py @@ -34,7 +34,7 @@ class IdentifierTest(unittest.TestCase): type=RelatedIdentifierType.DOI)], creators=[IdentifierCreator(id=5, creator_name='Carberry, Josiah')]) # mock - mock.post('/api/identifier', json=exp.model_dump_json(), status_code=201) + mock.post('/api/identifier', json=exp.model_dump(), status_code=201) # test client = RestClient(username="a", password="b") response = client.create_identifier(database_id=1, type=IdentifierType.VIEW, @@ -137,7 +137,7 @@ class IdentifierTest(unittest.TestCase): creators=[IdentifierCreator(id=5, creator_name='Carberry, Josiah', name_identifier='https://orcid.org/0000-0002-1825-0097')]) # mock - mock.get('/api/identifier?url=https://orcid.org/0000-0002-1825-0097', json=exp.model_dump_json()) + mock.get('/api/identifier?url=https://orcid.org/0000-0002-1825-0097', json=exp.model_dump()) # test response = RestClient().suggest_identifier("https://orcid.org/0000-0002-1825-0097") self.assertEqual(exp, response) diff --git a/lib/python/tests/test_query.py b/lib/python/tests/test_query.py index 76bd95d8d34e7531806191fb6214c6f45d526612..0d75b8afc5d34e18f432d38e6796541a4522c038 100644 --- a/lib/python/tests/test_query.py +++ b/lib/python/tests/test_query.py @@ -1,9 +1,12 @@ import unittest +from json import dumps +from typing import Any import requests_mock import datetime from dbrepo.RestClient import RestClient +from pandas import DataFrame from dbrepo.api.dto import Result, Query, User, UserAttributes, QueryType from dbrepo.api.exceptions import MalformedError, NotExistsError, ForbiddenError, QueryStoreError, \ @@ -18,7 +21,7 @@ class QueryTest(unittest.TestCase): headers=[{'id': 0, 'username': 1}], id=None) # mock - mock.post('/api/database/1/query', json=exp.model_dump_json(), status_code=202) + mock.post('/api/database/1/query', json=exp.model_dump(), status_code=202) # test client = RestClient(username="a", password="b") response = client.execute_query(database_id=1, page=0, size=10, @@ -114,7 +117,7 @@ class QueryTest(unittest.TestCase): result_number=None, identifiers=[]) # mock - mock.get('/api/database/1/query/6', json=exp.model_dump_json()) + mock.get('/api/database/1/query/6', json=exp.model_dump()) # test response = RestClient().get_query(database_id=1, query_id=6) self.assertEqual(exp, response) @@ -237,11 +240,24 @@ class QueryTest(unittest.TestCase): headers=[{'id': 0, 'username': 1}], id=6) # mock - mock.get('/api/database/1/query/6/data', json=exp.model_dump_json()) + mock.get('/api/database/1/query/6/data', json=exp.model_dump()) # test response = RestClient().get_query_data(database_id=1, query_id=6) self.assertEqual(exp, response) + def test_get_query_data_dataframe_succeeds(self): + with requests_mock.Mocker() as mock: + res = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], + headers=[{'id': 0, 'username': 1}], + id=6) + exp = DataFrame.from_records(res.model_dump()['result']) + # mock + mock.get('/api/database/1/query/6/data', json=res.model_dump()) + # test + response = RestClient().get_query_data(database_id=1, query_id=6, df=True) + self.assertEqual(exp.shape, response.shape) + self.assertTrue(DataFrame.equals(exp, response)) + def test_get_query_data_not_allowed_fails(self): with requests_mock.Mocker() as mock: # mock diff --git a/lib/python/tests/test_table.py b/lib/python/tests/test_table.py index 68f947386e4441a8aa290bf224b55cf15e26496c..286d908c917b857f5c939d1b83fbd968ead89890 100644 --- a/lib/python/tests/test_table.py +++ b/lib/python/tests/test_table.py @@ -1,9 +1,11 @@ import unittest +from json import dumps import requests_mock import datetime from dbrepo.RestClient import RestClient +from pandas import DataFrame from dbrepo.api.dto import Table, CreateTableConstraints, UserAttributes, User, Column, Constraints, ColumnType, Result, \ Concept, Unit, TableStatistics, ColumnStatistic @@ -42,7 +44,7 @@ class TableTest(unittest.TestCase): is_null_allowed=False)]) with requests_mock.Mocker() as mock: # mock - mock.post('/api/database/1/table', json=exp.model_dump_json(), status_code=201) + mock.post('/api/database/1/table', json=exp.model_dump(), status_code=201) # test client = RestClient(username="a", password="b") response = client.create_table(database_id=1, name="Test", description="Test Table", columns=[], @@ -179,7 +181,7 @@ class TableTest(unittest.TestCase): is_public=True, is_null_allowed=False)]) # mock - mock.get('/api/database/1/table/2', json=exp.model_dump_json()) + mock.get('/api/database/1/table/2', json=exp.model_dump()) # test response = RestClient().get_table(database_id=1, table_id=2) self.assertEqual(exp, response) @@ -250,11 +252,24 @@ class TableTest(unittest.TestCase): headers=[{'id': 0, 'username': 1}], id=None) # mock - mock.get('/api/database/1/table/9/data', json=exp.model_dump_json()) + mock.get('/api/database/1/table/9/data', json=exp.model_dump()) # test response = RestClient().get_table_data(database_id=1, table_id=9) self.assertEqual(exp, response) + def test_get_table_data_dataframe_succeeds(self): + with requests_mock.Mocker() as mock: + res = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], + headers=[{'id': 0, 'username': 1}], + id=None) + exp = DataFrame.from_records(res.model_dump()['result']) + # mock + mock.get('/api/database/1/table/9/data', json=res.model_dump()) + # test + response = RestClient().get_table_data(database_id=1, table_id=9, df=True) + self.assertEqual(exp.shape, response.shape) + self.assertTrue(DataFrame.equals(exp, response)) + def test_get_table_data_malformed_fails(self): with requests_mock.Mocker() as mock: # mock @@ -558,7 +573,7 @@ class TableTest(unittest.TestCase): name="liters per square meter"), is_null_allowed=False) # mock - mock.put('/api/database/1/table/2/column/1', json=exp.model_dump_json(), status_code=202) + mock.put('/api/database/1/table/2/column/1', json=exp.model_dump(), status_code=202) # test client = RestClient(username="a", password="b") response = client.update_table_column(database_id=1, table_id=2, column_id=1, @@ -622,7 +637,7 @@ class TableTest(unittest.TestCase): exp = TableStatistics( columns={"id": ColumnStatistic(val_min=1.0, val_max=9.0, mean=5.0, median=5.0, std_dev=2.73)}) # mock - mock.get('/api/analyse/database/1/table/2/statistics', json=exp.model_dump_json(), status_code=202) + mock.get('/api/analyse/database/1/table/2/statistics', json=exp.model_dump(), status_code=202) # test response = RestClient().analyse_table_statistics(database_id=1, table_id=2) self.assertEqual(exp, response) diff --git a/lib/python/tests/test_user.py b/lib/python/tests/test_user.py index d5afaf2d01c90517dee6ecf3fffeec96347d466b..67698a8fd58b811f58a89a7ba9d1c5245f2f242c 100644 --- a/lib/python/tests/test_user.py +++ b/lib/python/tests/test_user.py @@ -53,7 +53,7 @@ class UserTest(unittest.TestCase): with requests_mock.Mocker() as mock: exp = UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise') # mock - mock.post('http://gateway-service/api/user', json=exp.model_dump_json(), status_code=201) + mock.post('http://gateway-service/api/user', json=exp.model_dump(), status_code=201) # test response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') self.assertEqual(exp, response) @@ -62,7 +62,7 @@ class UserTest(unittest.TestCase): with requests_mock.Mocker() as mock: exp = UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise') # mock - mock.post('http://gateway-service/api/user', json=exp.model_dump_json(), status_code=400) + mock.post('http://gateway-service/api/user', json=exp.model_dump(), status_code=400) # test try: response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') @@ -73,7 +73,7 @@ class UserTest(unittest.TestCase): with requests_mock.Mocker() as mock: exp = UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise') # mock - mock.post('http://gateway-service/api/user', json=exp.model_dump_json(), status_code=403) + mock.post('http://gateway-service/api/user', json=exp.model_dump(), status_code=403) # test try: response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') @@ -84,7 +84,7 @@ class UserTest(unittest.TestCase): with requests_mock.Mocker() as mock: exp = UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise') # mock - mock.post('http://gateway-service/api/user', json=exp.model_dump_json(), status_code=409) + mock.post('http://gateway-service/api/user', json=exp.model_dump(), status_code=409) # test try: response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') @@ -95,7 +95,7 @@ class UserTest(unittest.TestCase): with requests_mock.Mocker() as mock: exp = UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise') # mock - mock.post('http://gateway-service/api/user', json=exp.model_dump_json(), status_code=404) + mock.post('http://gateway-service/api/user', json=exp.model_dump(), status_code=404) # test try: response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') @@ -106,7 +106,7 @@ class UserTest(unittest.TestCase): with requests_mock.Mocker() as mock: exp = UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise') # mock - mock.post('http://gateway-service/api/user', json=exp.model_dump_json(), status_code=417) + mock.post('http://gateway-service/api/user', json=exp.model_dump(), status_code=417) # test try: response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') @@ -119,7 +119,7 @@ class UserTest(unittest.TestCase): attributes=UserAttributes(theme='dark')) # mock mock.get('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', - json=exp.model_dump_json()) + json=exp.model_dump()) # test response = RestClient().get_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16') self.assertEqual(exp, response) @@ -140,7 +140,7 @@ class UserTest(unittest.TestCase): attributes=UserAttributes(theme='dark')) # mock mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', status_code=202, - json=exp.model_dump_json()) + json=exp.model_dump()) # test client = RestClient(username="a", password="b") response = client.update_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', firstname='Martin') @@ -195,7 +195,7 @@ class UserTest(unittest.TestCase): attributes=UserAttributes(theme='dark')) # mock mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/theme', status_code=202, - json=exp.model_dump_json()) + json=exp.model_dump()) # test client = RestClient(username="a", password="b") response = client.update_user_theme(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', theme='dark') @@ -250,7 +250,7 @@ class UserTest(unittest.TestCase): attributes=UserAttributes(theme='dark')) # mock mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/password', status_code=202, - json=exp.model_dump_json()) + json=exp.model_dump()) # test client = RestClient(username="a", password="b") response = client.update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', diff --git a/lib/python/tests/test_view.py b/lib/python/tests/test_view.py index 16eb6f4c518aa79c8d1e9ed0d9d7aed1c599614b..2a61cab89ee2a4a0fa1778c598da27e00ea9910b 100644 --- a/lib/python/tests/test_view.py +++ b/lib/python/tests/test_view.py @@ -1,9 +1,11 @@ import unittest +from json import dumps import requests_mock import datetime from dbrepo.RestClient import RestClient +from pandas import DataFrame from dbrepo.api.dto import UserAttributes, User, View, Result from dbrepo.api.exceptions import ForbiddenError, NotExistsError, MalformedError, AuthenticationError @@ -76,7 +78,7 @@ class ViewTest(unittest.TestCase): last_modified=datetime.datetime(2024, 1, 1, 0, 0, 0, 0, datetime.timezone.utc), identifiers=[]) # mock - mock.get('/api/database/1/view/3', json=exp.model_dump_json()) + mock.get('/api/database/1/view/3', json=exp.model_dump()) # test response = RestClient().get_view(database_id=1, view_id=3) self.assertEqual(exp, response) @@ -117,7 +119,7 @@ class ViewTest(unittest.TestCase): last_modified=datetime.datetime(2024, 1, 1, 0, 0, 0, 0, datetime.timezone.utc), identifiers=[]) # mock - mock.post('/api/database/1/view', json=exp.model_dump_json(), status_code=201) + mock.post('/api/database/1/view', json=exp.model_dump(), status_code=201) # test client = RestClient(username="a", password="b") response = client.create_view(database_id=1, name="Data", is_public=True, @@ -217,11 +219,24 @@ class ViewTest(unittest.TestCase): headers=[{'id': 0, 'username': 1}], id=None) # mock - mock.get('/api/database/1/view/3/data', json=exp.model_dump_json()) + mock.get('/api/database/1/view/3/data', json=exp.model_dump()) # test response = RestClient().get_view_data(database_id=1, view_id=3) self.assertEqual(exp, response) + def test_get_view_data_dataframe_succeeds(self): + with requests_mock.Mocker() as mock: + res = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], + headers=[{'id': 0, 'username': 1}], + id=None) + exp = DataFrame.from_records(res.model_dump()['result']) + # mock + mock.get('/api/database/1/view/3/data', json=res.model_dump()) + # test + response: DataFrame = RestClient().get_view_data(database_id=1, view_id=3, df=True) + self.assertEqual(exp.shape, response.shape) + self.assertTrue(DataFrame.equals(exp, response)) + def test_get_view_data_malformed_fails(self): with requests_mock.Mocker() as mock: # mock