From 68f3206e99bf21dc5b91d51011d50d1b1a20b6f3 Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Mon, 25 Mar 2024 07:37:08 +0000 Subject: [PATCH] Dev --- .docs/stylesheets/extra.css | 2 +- .docs/system-services-broker.md | 2 +- .docs/usage-analyse.md | 34 - .docs/usage-broker.md | 52 - .docs/usage-metadata.md | 23 - .docs/usage-overview.md | 687 +++----- .docs/usage-python.md | 124 ++ .docs/usage-search.md | 9 - .gitlab-ci.yml | 66 + Makefile | 11 +- Pipfile.lock | 1003 ------------ dbrepo-analyse-service/.gitignore | 1 - dbrepo-analyse-service/Pipfile | 2 + dbrepo-analyse-service/Pipfile.lock | 234 ++- dbrepo-analyse-service/app.py | 139 +- .../as-yml/analyse_datatypes.yml | 103 ++ .../as-yml/analyse_keys.yml | 85 + .../as-yml/analyse_table_stat.yml | 77 + .../as-yml/determine_stat.yml | 52 - .../as-yml/determine_stats.yml | 24 - dbrepo-analyse-service/as-yml/determinedt.yml | 60 - dbrepo-analyse-service/as-yml/determinepk.yml | 27 - dbrepo-analyse-service/determine_pk.py | 15 +- dbrepo-analyse-service/determine_stats.py | 37 +- .../test/test_determine_pk.py | 24 +- dbrepo-gateway-service/dbrepo.conf | 2 +- dbrepo-metadata-db/1_setup-schema.sql | 4 +- .../container/ContainerCreateRequestDto.java | 25 + .../at/tuwien/api/container/ContainerDto.java | 1 + .../tuwien/api/database/DatabaseBriefDto.java | 83 - .../at/tuwien/api/database/DatabaseDto.java | 2 + .../at/tuwien/api/database/ViewBriefDto.java | 5 +- .../api/database/query/QueryBriefDto.java | 3 +- .../tuwien/api/database/query/QueryDto.java | 2 + .../api/database/query/QueryResultDto.java | 4 - .../tuwien/api/database/table/TableDto.java | 2 + .../constraints/ConstraintsCreateDto.java | 3 +- .../table/constraints/ConstraintsDto.java | 3 +- .../constraints/foreignKey/ForeignKeyDto.java | 4 + .../tuwien/api/identifier/CreatorSaveDto.java | 3 - .../api/identifier/IdentifierBriefDto.java | 72 - .../tuwien/api/identifier/IdentifierDto.java | 6 +- .../IdentifierSaveDescriptionDto.java | 2 + .../api/identifier/IdentifierSaveDto.java | 3 + .../identifier/IdentifierSaveTitleDto.java | 2 + .../api/identifier/RelatedIdentifierDto.java | 14 +- .../identifier/RelatedIdentifierSaveDto.java | 2 + .../at/tuwien/api/user/UserAttributesDto.java | 2 + .../FormatNotAvailableException.java | 23 + .../java/at/tuwien/mapper/DatabaseMapper.java | 16 +- .../at/tuwien/mapper/IdentifierMapper.java | 4 - .../java/at/tuwien/mapper/QueryMapper.java | 63 +- .../java/at/tuwien/mapper/TableMapper.java | 5 +- .../at/tuwien/endpoints/DatabaseEndpoint.java | 50 +- .../tuwien/endpoints/IdentifierEndpoint.java | 29 +- .../tuwien/endpoints/PersistenceEndpoint.java | 19 +- .../at/tuwien/endpoints/QueryEndpoint.java | 74 +- .../at/tuwien/endpoints/StoreEndpoint.java | 20 +- .../tuwien/endpoints/TableDataEndpoint.java | 77 +- .../at/tuwien/endpoints/TableEndpoint.java | 6 +- .../at/tuwien/endpoints/ViewEndpoint.java | 75 +- .../endpoints/DatabaseEndpointUnitTest.java | 94 +- .../endpoints/QueryEndpointUnitTest.java | 28 +- .../endpoints/TableDataEndpointUnitTest.java | 79 +- .../endpoints/TableEndpointUnitTest.java | 5 +- .../endpoints/ViewEndpointUnitTest.java | 157 +- .../tuwien/mvc/PrometheusEndpointMvcTest.java | 34 +- .../DatabaseServiceIntegrationTest.java | 13 + .../service/DatabaseServiceUnitTest.java | 2 +- .../IdentifierServiceIntegrationTest.java | 33 - .../service/MetadataServiceUnitTest.java | 19 +- .../service/QueryServiceIntegrationTest.java | 12 +- .../TableServiceIntegrationWriteTest.java | 4 +- .../at/tuwien/service/IdentifierService.java | 4 +- .../at/tuwien/service/MetadataService.java | 10 +- .../java/at/tuwien/service/TableService.java | 2 +- .../service/impl/ContainerServiceImpl.java | 6 + .../impl/DataCiteIdentifierServiceImpl.java | 2 +- .../service/impl/IdentifierServiceImpl.java | 2 +- .../service/impl/MariaDbServiceImpl.java | 38 +- .../service/impl/MetadataServiceImpl.java | 4 +- .../tuwien/service/impl/QueryServiceImpl.java | 5 +- .../tuwien/service/impl/TableServiceImpl.java | 24 +- .../main/java/at/tuwien/test/BaseTest.java | 44 +- dbrepo-ui/bun.lockb | Bin 363576 -> 363576 bytes dbrepo-ui/components/dialogs/TimeTravel.vue | 2 +- dbrepo-ui/components/subset/Results.vue | 5 +- dbrepo-ui/components/table/TableImport.vue | 3 +- dbrepo-ui/composables/analyse-service.ts | 2 +- dbrepo-ui/composables/axios-instance.ts | 2 +- dbrepo-ui/composables/database-service.ts | 44 +- dbrepo-ui/composables/query-service.ts | 8 +- dbrepo-ui/composables/table-service.ts | 2 +- dbrepo-ui/composables/upload-service.ts | 4 +- dbrepo-ui/composables/view-service.ts | 9 +- dbrepo-ui/dto/index.ts | 132 +- .../[database_id]/view/[view_id]/info.vue | 2 +- dbrepo-ui/pages/search.vue | 7 +- docker-compose.prod.yml | 4 +- docker-compose.yml | 8 +- helm-charts/dbrepo/Chart.lock | 4 +- lib/.gitignore | 7 + lib/python/.gitignore | 20 + lib/python/.pypirc | 18 + lib/python/LICENSE | 202 +++ lib/python/Makefile | 26 + lib/python/Pipfile | 23 + lib/python/Pipfile.lock | 1340 +++++++++++++++ lib/python/README.md | 90 ++ lib/python/build-website.sh | 8 + lib/python/build.sh | 8 + lib/python/dbrepo/AmqpClient.py | 62 + lib/python/dbrepo/RestClient.py | 1435 +++++++++++++++++ .../test => lib/python/dbrepo}/__init__.py | 0 Pipfile => lib/python/dbrepo/api/__init__.py | 0 lib/python/dbrepo/api/dto.py | 1096 +++++++++++++ lib/python/dbrepo/api/exceptions.py | 82 + lib/python/debug.py | 6 + lib/python/docs/Makefile | 20 + lib/python/docs/conf.py | 54 + lib/python/docs/guide/amqp-client.rst | 9 + lib/python/docs/guide/rest-client.rst | 9 + lib/python/docs/index.rst | 36 + lib/python/docs/make.bat | 35 + lib/python/docs/source/dbrepo.api.rst | 29 + lib/python/docs/source/dbrepo.rst | 29 + lib/python/docs/source/modules.rst | 7 + lib/python/docs/static/css/custom.css | 105 ++ lib/python/docs/static/theme_dark_logo.png | Bin 0 -> 19636 bytes lib/python/docs/static/theme_light_logo.png | Bin 0 -> 22600 bytes .../docs/templates/sidebar/feedback.html | 7 + lib/python/pyproject.toml | 39 + lib/python/sensor.csv | 5 + lib/python/setup.py | 14 + lib/python/test.sh | 3 + lib/python/tests/test_analyse.py | 24 + lib/python/tests/test_database.py | 572 +++++++ lib/python/tests/test_identifier.py | 195 +++ lib/python/tests/test_license.py | 33 + lib/python/tests/test_query.py | 335 ++++ lib/python/tests/test_rest_client.py | 41 + lib/python/tests/test_table.py | 651 ++++++++ lib/python/tests/test_user.py | 321 ++++ lib/python/tests/test_view.py | 275 ++++ mkdocs.yml | 6 +- 145 files changed, 8778 insertions(+), 2961 deletions(-) delete mode 100644 .docs/usage-analyse.md delete mode 100644 .docs/usage-broker.md delete mode 100644 .docs/usage-metadata.md create mode 100644 .docs/usage-python.md delete mode 100644 .docs/usage-search.md delete mode 100644 Pipfile.lock create mode 100644 dbrepo-analyse-service/as-yml/analyse_datatypes.yml create mode 100644 dbrepo-analyse-service/as-yml/analyse_keys.yml create mode 100644 dbrepo-analyse-service/as-yml/analyse_table_stat.yml delete mode 100644 dbrepo-analyse-service/as-yml/determine_stat.yml delete mode 100644 dbrepo-analyse-service/as-yml/determine_stats.yml delete mode 100644 dbrepo-analyse-service/as-yml/determinedt.yml delete mode 100644 dbrepo-analyse-service/as-yml/determinepk.yml delete mode 100644 dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseBriefDto.java delete mode 100644 dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierBriefDto.java create mode 100644 dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/FormatNotAvailableException.java create mode 100644 lib/.gitignore create mode 100644 lib/python/.gitignore create mode 100644 lib/python/.pypirc create mode 100644 lib/python/LICENSE create mode 100644 lib/python/Makefile create mode 100644 lib/python/Pipfile create mode 100644 lib/python/Pipfile.lock create mode 100644 lib/python/README.md create mode 100755 lib/python/build-website.sh create mode 100644 lib/python/build.sh create mode 100644 lib/python/dbrepo/AmqpClient.py create mode 100644 lib/python/dbrepo/RestClient.py rename {dbrepo-analyse-service/test => lib/python/dbrepo}/__init__.py (100%) rename Pipfile => lib/python/dbrepo/api/__init__.py (100%) create mode 100644 lib/python/dbrepo/api/dto.py create mode 100644 lib/python/dbrepo/api/exceptions.py create mode 100644 lib/python/debug.py create mode 100644 lib/python/docs/Makefile create mode 100644 lib/python/docs/conf.py create mode 100644 lib/python/docs/guide/amqp-client.rst create mode 100644 lib/python/docs/guide/rest-client.rst create mode 100644 lib/python/docs/index.rst create mode 100644 lib/python/docs/make.bat create mode 100644 lib/python/docs/source/dbrepo.api.rst create mode 100644 lib/python/docs/source/dbrepo.rst create mode 100644 lib/python/docs/source/modules.rst create mode 100644 lib/python/docs/static/css/custom.css create mode 100644 lib/python/docs/static/theme_dark_logo.png create mode 100755 lib/python/docs/static/theme_light_logo.png create mode 100644 lib/python/docs/templates/sidebar/feedback.html create mode 100644 lib/python/pyproject.toml create mode 100644 lib/python/sensor.csv create mode 100644 lib/python/setup.py create mode 100644 lib/python/test.sh create mode 100644 lib/python/tests/test_analyse.py create mode 100644 lib/python/tests/test_database.py create mode 100644 lib/python/tests/test_identifier.py create mode 100644 lib/python/tests/test_license.py create mode 100644 lib/python/tests/test_query.py create mode 100644 lib/python/tests/test_rest_client.py create mode 100644 lib/python/tests/test_table.py create mode 100644 lib/python/tests/test_user.py create mode 100644 lib/python/tests/test_view.py diff --git a/.docs/stylesheets/extra.css b/.docs/stylesheets/extra.css index 6d045c82da..b6614e1400 100644 --- a/.docs/stylesheets/extra.css +++ b/.docs/stylesheets/extra.css @@ -28,4 +28,4 @@ figure img.img-border { [data-md-component=announce] .md-banner__inner { margin-top: 0.2rem; margin-bottom: 0.2rem; -} +} \ No newline at end of file diff --git a/.docs/system-services-broker.md b/.docs/system-services-broker.md index cc4b775c1f..7ed0c3333f 100644 --- a/.docs/system-services-broker.md +++ b/.docs/system-services-broker.md @@ -8,7 +8,7 @@ author: Martin Weise !!! debug "Debug Information" - Image: [`bitnami/rabbitmq:3.10`](https://hub.docker.com/r/bitnami/rabbitmq) + Image: [`bitnami/rabbitmq:3.12.13-debian-12-r2`](https://hub.docker.com/r/bitnami/rabbitmq) * Ports: 5672/tcp, 15672/tcp, 15692/tcp * AMQP: `amqp://<hostname>:5672` diff --git a/.docs/usage-analyse.md b/.docs/usage-analyse.md deleted file mode 100644 index cc74d3bcf3..0000000000 --- a/.docs/usage-analyse.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -author: Martin Weise ---- - -# Analyse Service - -Given a [CSV-file](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-datasets/-/raw/master/gps.csv) -containing GPS-data `gps.csv` already uploaded in the `dbrepo-upload` bucket of the Storage Service with key `gps.csv`: - -=== "Terminal" - - ```shell - curl -X POST \ - -d '{"filename":"gps.csv","separator":","}' - http://<hostname>/api/analyse/determinedt - ``` - - This results in the response: - - ```json - { - "columns": { - "ID": "bigint", - "KEY": "varchar", - "OBJECTID": "bigint", - "LBEZEICHNUNG": "varchar", - "LTYP": "bigint", - "LTYPTXT": "varchar", - "LAT": "decimal", - "LNG": "decimal" - }, - "separator": "," - } - ``` \ No newline at end of file diff --git a/.docs/usage-broker.md b/.docs/usage-broker.md deleted file mode 100644 index 67f063be62..0000000000 --- a/.docs/usage-broker.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -author: Martin Weise ---- - -# Broker Service - -## Preliminary - -The RabbitMQ client can be authenticated through Basic Authentication (username, password) and Bearer Authentication. - -!!! example "Bearer Authentication" - - Note that the encoded/signed `ACCESS_TOKEN` already contains a field `client_id=username`, so the username is - optional in `PlainCredentials` when using Bearer Authentication, but provided must match the username. - -=== "Bearer Authentication" - - ```python - import pika - - # Configure client - credentials = pika.credentials.PlainCredentials("", "ACCESS_TOKEN") - parameters = pika.ConnectionParameters('localhost', 5672, '/', credentials) - connection = pika.BlockingConnection(parameters) - - # Channel - channel = connection.channel() - channel.basic_publish(exchange='dbrepo', - routing_key='dbrepo.database_name.table_name', - body=b'Hello World!') - print(" [x] Sent 'Hello World!'") - connection.close() - ``` - -=== "Basic Authentication" - - ```python - import pika - - # Configure client - credentials = pika.credentials.PlainCredentials("username", "password") - parameters = pika.ConnectionParameters('localhost', 5672, '/', credentials) - connection = pika.BlockingConnection(parameters) - - # Channel - channel = connection.channel() - channel.basic_publish(exchange='dbrepo', - routing_key='dbrepo.database_name.table_name', - body=b'Hello World!') - print(" [x] Sent 'Hello World!'") - connection.close() - ``` diff --git a/.docs/usage-metadata.md b/.docs/usage-metadata.md deleted file mode 100644 index 690e07f4b0..0000000000 --- a/.docs/usage-metadata.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -author: Martin Weise ---- - -# Metadata Service - -## Preliminary - -<!-- md:version 1.4.1 --> - -!!! example "Basic Authentication" - - The use of **Basic Authentication** (username, password) instead of *Bearer Authentication* may be useful for - applications that do not have the technical capability of refreshing tokens in intervals (e.g. single-threaded - applications). It is however not recommended for any other applications as **Basic Authentication** transmits the - user password with every request. - - Additionally, performance is decreased as with every **Basic Authentication** request, an additional request is - sent to the [Authentication Service](../system-services-authentication/) where the authorization is requested before - authentication to the Metadata Service. This performance degradation should be avoided whenever possible. Use - **Bearer Authentication** instead, see how to - [obtain an access token](../usage-authentication/#obtain-access-token). - diff --git a/.docs/usage-overview.md b/.docs/usage-overview.md index f62ca1967c..8b8d7218b5 100644 --- a/.docs/usage-overview.md +++ b/.docs/usage-overview.md @@ -4,6 +4,9 @@ author: Martin Weise # Overview +We developed a Python Library for communicating with DBRepo from e.g. Jupyter Notebooks. See +the [Python Library](../usage-python) page for more details. + We give usage examples of the most important use-cases we identified. | | UI | Terminal | JDBC | Python | @@ -63,31 +66,17 @@ A user wants to create an account in DBRepo. curl -sSL \ -X POST \ -H "Content-Type: application/json" \ - -d '{"username":"foo","email":"foo.bar@example.com","password":"bar"}' \ - http://localhost/api/user | jq .id - ``` - - To login in DBRepo, obtain an access token: - - ```bash - curl -sSL \ - -X POST \ - -d 'username=foo&password=bar&grant_type=password&client_id=dbrepo-client&scope=openid&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG' \ - http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token | jq .access_token + -d '{"username": "foo","email": "foo.bar@example.com", "password":"bar"}' \ + http://<hostname>/api/user | jq .id ``` - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - You can view your user information by sending a request to the user endpoint with your access token. ```bash curl -sSL \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - http://localhost/api/user/USER_ID | jq + -H "Content-Type: application/json" \ + -u "foo:bar" \ + http://<hostname>/api/user/14824cc5-186b-423d-a7b7-a252aed42b59 | jq ``` To change your password in all components of DBRepo: @@ -95,9 +84,10 @@ A user wants to create an account in DBRepo. ```bash curl -sSL \ -X PUT \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - -d '{"password":"s3cr3tp4ss0rd"}' \ - http://localhost/api/user/USER_ID/password | jq + -H "Content-Type: application/json" \ + -u "foo:bar" \ + -d '{"password": "s3cr3tp4ss0rd"}' \ + http://<hostname>/api/user/USER_ID/password | jq ``` === "Python" @@ -105,62 +95,34 @@ A user wants to create an account in DBRepo. To create a new user account in DBRepo with the Terminal, provide your details in the following REST HTTP-API call. ```python - import requests - - response = requests.post("http://localhost/api/user", json={ - "username": "foo", - "password": "bar", - "email": "foo.bar@example.com" - }) - user_id = response.json()["id"] - print(user_id) - ``` - - To login in DBRepo, obtain an access token: + from dbrepo.RestClient import RestClient - ```python - import requests - - response = requests.post("http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token", json={ - "username": "foo", - "password": "bar", - "grant_type": "password", - "client_id": "dbrepo-client", - "scope": "openid", - "client_secret": "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" - }) - access_token = response.json()["access_token"] - print(access_token) + client = RestClient(endpoint="http://<hostname>") + user = client.create_user(username="foo", password="bar", + email="foo@example.com") + print(f"user id: {user.id}") ``` - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - You can view your user information by sending a request to the user endpoint with your access token. ```python - import requests - - response = requests.get("http://localhost/api/user/" + user_id, header={ - "Authorization": "Bearer " + access_token - }) - user_id = response.json()["id"] - print(user_id) + from dbrepo.RestClient import RestClient + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + user = client.get_user(user_id="57ea75c6-2a1d-4516-89f4-296b8b62539b") + print(f"user info: {user}") ``` To change your password in all components of DBRepo: ```python - import requests - - requests.put("http://localhost/api/user/" + user_id + "/password", header={ - "Authorization": "Bearer " + access_token - }, json={ - "password": "s3cr3tp4ssw0rd" - }) + from dbrepo.RestClient import RestClient + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + client.update_user_password(user_id="57ea75c6-2a1d-4516-89f4-296b8b62539b", + password="baz") ``` ## Create Database @@ -194,26 +156,12 @@ A user wants to create a database in DBRepo. === "Terminal" - Obtain an access token: - - ```bash - curl -sSL \ - -X POST \ - -d 'username=foo&password=bar&grant_type=password&client_id=dbrepo-client&scope=openid&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG' \ - http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token | jq .access_token - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - Then list all available containers with their database engine descriptions and obtain a container id. ```bash curl -sSL \ - http://localhost/api/container | jq + -H "Content-Type: application/json" \ + http://<hostname>/api/container | jq ``` Create a public databse with the container id from the previous step. You can also create a private database in this @@ -222,59 +170,35 @@ A user wants to create a database in DBRepo. ```bash curl -sSL \ -X POST \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - -d '{"name":"Danube Water Quality Measurements","container_id":1,"is_public":true}' \ - http://localhost/api/database | jq .id + -H "Content-Type: application/json" \ + -u "foo:bar" \ + -d '{"name":"Danube Water Quality Measurements", "container_id":1, "is_public":true}' \ + http://<hostname>/api/database | jq .id ``` === "Python" - Obtain an access token: + List all available containers with their database engine descriptions and obtain a container id. ```python - import requests - - response = requests.post("http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token", json={ - "username": "foo", - "password": "bar", - "grant_type": "password", - "client_id": "dbrepo-client", - "scope": "openid", - "client_secret": "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" - }) - access_token = response.json()["access_token"] - print(access_token) - ``` - - !!! note + from dbrepo.RestClient import RestClient - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - - Then list all available containers with their database engine descriptions and obtain a container id. - - ```python - import requests - - response = requests.get("http://localhost/api/container") - print(response.json()) + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + containers = client.get_containers() + print(containers) ``` Create a public databse with the container id from the previous step. You can also create a private database in this step, others can still see the metadata. ```python - import requests - - response = requests.post("http://localhost/api/database", headers={ - "Authorization": "Bearer " + access_token - }, json={ - "name": "Danube Water Quality Measurements", - "container_id": 1, - "is_public": True - }) - print(response.json()["id"]) + from dbrepo.RestClient import RestClient + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + database = client.create_database(name="Test", container_id=1, is_public=True) + print(f"database id: {database.id}") ``` ## Import Dataset @@ -366,28 +290,13 @@ access to. This is the default for self-created databases like above in [Create === "Terminal" - Obtain an access token: - - ```bash - curl -sSL \ - -X POST \ - -d 'username=foo&password=bar&grant_type=password&client_id=dbrepo-client&scope=openid&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG' \ - http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token | jq .access_token - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - Select a database where you have at least `write-all` access (this is the case for e.g. self-created databases). Upload the dataset via the [`tusc`](https://github.com/adhocore/tusc.sh) terminal application or use Python and copy the file key. ```bash - tusc -H http://localhost/api/upload/files -f danube.csv -b /dbrepo-upload/ + tusc -H http://<hostname>/api/upload/files -f danube.csv -b /dbrepo-upload/ ``` Analyse the dataset and get the table column names and datatype suggestion. @@ -395,9 +304,10 @@ access to. This is the default for self-created databases like above in [Create ```bash curl -sSL \ -X POST \ - -H "Authorization: Bearer ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -u "foo:bar" \ -d '{"filename":"FILEKEY","separator":","}' \ - http://localhost/api/analyse/determinedt | jq + http://<hostname>/api/analyse/datatypes | jq ``` Provide the table name and optionally a table description along with the table columns. @@ -405,9 +315,10 @@ access to. This is the default for self-created databases like above in [Create ```bash curl -sSL \ -X POST \ - -H "Authorization: Bearer ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -u "foo:bar" \ -d '{"name":"Danube water levels","description":"Measurements of the river danube water levels","columns":[{"name":"datetime","type":"timestamp","dfid":1,"primary_key":false,"null_allowed":true},{"name":"level","type":"bigint","size":255,"primary_key":false,"null_allowed":true}]}' \ - http://localhost/api/database/1/table | jq .id + http://<hostname>/api/database/1/table | jq .id ``` Next, provide the dataset metadata that is necessary for import into the table by providing the dataset separator @@ -422,9 +333,10 @@ access to. This is the default for self-created databases like above in [Create ```bash curl -sSL \ -X POST \ - -H "Authorization: Bearer ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -u "foo:bar" \ -d '{"location":"FILEKEY","separator":",","quote":"\"","skip_lines":1,"null_element":"NA"}' \ - http://localhost/api/database/1/table/1/data/import | jq + http://<hostname>/api/database/1/table/1/data/import | jq ``` When you are finished with the table schema definition, the dataset is imported and a table is created. View the @@ -432,75 +344,42 @@ access to. This is the default for self-created databases like above in [Create ```bash curl -sSL \ - http://localhost/api/database/1/table/1/data?page=0&size=10 | jq + -H "Content-Type: application/json" \ + -u "foo:bar" \ + http://<hostname>/api/database/1/table/1/data?page=0&size=10 | jq ``` === "Python" - Obtain an access token: - - ```python - import requests - - response = requests.post("http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token", json={ - "username": "foo", - "password": "bar", - "grant_type": "password", - "client_id": "dbrepo-client", - "scope": "openid", - "client_secret": "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" - }) - access_token = response.json()["access_token"] - print(access_token) - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - Select a database where you have at least `write-all` access (this is the case for e.g. self-created databases). - - Upload the dataset via the Python [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) - client to the `dbrepo-upload` bucket. + Upload the dataset to analyse the dataset and get the table column names and datatype suggestion. ```python - import boto3 - import os - - client = boto3.client(service_name='s3', endpoint_url='http://localhost:9000', - aws_access_key_id='seaweedfsadmin', - aws_secret_access_key='seaweedfsadmin') - filepath = os.path.join('/path/to', 'your_dataset.csv') - client.upload_file(filepath, 'dbrepo-upload', 'your_dataset.csv') - ``` - - Analyse the dataset and get the table column names and datatype suggestion. + from dbrepo.RestClient import RestClient - ```python - import requests - - response = requests.post("http://localhost/api/analyse/determinedt", headers={ - "Authorization": "Bearer " + access_token - }, json={ - "filename": "your_dataset.csv", - "separator": "," - }) - print(response.json()["columns"]) + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + analysis = client.analyse_datatypes(file_path="/path/to/dataset.csv", + separator=",") + print(f"analysis: {analysis}") + # separator=, columns={(id, bigint), ...}, line_termination=\n ``` Provide the table name and optionally a table description along with the table columns. ```python - import requests - - response = requests.post("http://localhost/api/database/1/table", headers={ - "Authorization": "Bearer " + access_token - }, json={"name": "Danube water levels", "description": "Measurements of the river danube water levels", - "columns": [{"name": "datetime", "type": "timestamp", "dfid": 1, "primary_key": False, "null_allowed": True}, - {"name": "level", "type": "bigint", "size": 255, "primary_key": False, "null_allowed": True}]}) - print(response.json()["id"]) + from dbrepo.RestClient import RestClient + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + table = client.create_table(database_id=1, + name="Sensor Data", + constraints=CreateTableConstraints(), + columns=[CreateTableColumn(name="id", + type=ColumnType.BIGINT, + primary_key=True, + null_allowed=False)]) + print(f"table id: {table.id}") ``` Next, provide the dataset metadata that is necessary for import into the table by providing the dataset separator @@ -513,28 +392,24 @@ access to. This is the default for self-created databases like above in [Create (e.g. `0` or `NO`), provide this information. ```python - import requests - - response = requests.post("http://localhost/api/database/1/table/1/data/import", headers={ - "Authorization": "Bearer " + access_token - }, json={ - "location": "your_dataset.csv", - "separator": ",", - "quote": "\"", - "skip_lines": 1, - "null_element": "NA" - }) + from dbrepo.RestClient import RestClient + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + client.import_table_data(database_id=1, table_id=3, + file_path="/path/to/dataset.csv", separator=",", + skip_lines=1, line_encoding="\n") ``` When you are finished with the table schema definition, the dataset is imported and a table is created. View the table data: ```python - import requests + from dbrepo.RestClient import RestClient - response = requests.get("http://localhost/api/database/1/table/1/data?page=0&size=10", headers={ - "Authorization": "Bearer " + access_token - }) + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + client.get_table_data(database_id=1, table_id=3) ``` ## Import Database Dump @@ -618,35 +493,15 @@ A user wants to import live data from e.g. sensor measurements fast and without === "Terminal" - Obtain an access token: - - ```bash - curl -sSL \ - -X POST \ - -d 'username=foo&password=bar&grant_type=password&client_id=dbrepo-client&scope=openid&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG' \ - http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token | jq .access_token - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - - !!! warning - - Beware that access tokens are short lived (default is 15 minutes) and need to be refreshed regularly with - refresh tokens (default is 10 days). See the usage page - on [how to refresh access tokens](../usage-authentication/#refresh-access-token). - Add a data tuple to an already existing table where the user has at least `write-own` access. ```bash curl -sSL \ -X POST \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - -d '{"data":{"column1":"value1","column2":"value2"}}' \ - http://localhost/api/database/1/table/1/data + -H "Content-Type: application/json" \ + -u "foo:bar" \ + -d '{"data":{"column1": "value1", "column2": "value2"}}' \ + http://<hostname>/api/database/1/table/1/data ``` === "JDBC API" @@ -676,19 +531,12 @@ A user wants to import live data from e.g. sensor measurements fast and without </figure> ```python - import pika - import json - - credentials = pika.credentials.PlainCredentials('foo', 'bar') - # credentials = pika.credentials.PlainCredentials('', 'ACCESS_TOKEN') - parameters = pika.ConnectionParameters('localhost', 5672, '/', credentials) - connection = pika.BlockingConnection(parameters) - channel = connection.channel() - channel.basic_publish(exchange='dbrepo', - routing_key='dbrepo.test_fiyg.dabube_water_levels', - body=json.dumps({"data":{"column1":"value1","column2":"value2"}})) - print(" [x] Sent tuple!") - connection.close() + from dbrepo.AmqpClient import AmqpClient + + client = AmqpClient(broker_host="test.dbrepo.tuwien.ac.at", username="foo", + password="bar") + client.publish(exchange="dbrepo", routing_key="dbrepo.database_feed.test", + data={'precipitation': 2.4}) ``` ## Export Subset @@ -742,30 +590,16 @@ A user wants to create a subset and export it as csv file. === "Terminal" - Obtain an access token: - - ```bash - curl -sSL \ - -X POST \ - -d 'username=foo&password=bar&grant_type=password&client_id=dbrepo-client&scope=openid&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG' \ - http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token | jq .access_token - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - A subset can be created by passing a SQL query to a database where you have at least `read` access (this is the case for e.g. self-created databases). ```bash curl -sSL \ -X POST \ - -H "Authorization: Bearer ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -u "foo:bar" \ -d '{"statement":"SELECT `id`,`datetime`,`level` FROM `danube_water_levels`"}' \ - http://localhost/api/database/1/query?page=0&sort=10 | jq .id + http://<hostname>/api/database/1/query?page=0&sort=10 | jq .id ``` !!! note @@ -776,8 +610,9 @@ A user wants to create a subset and export it as csv file. ```bash curl -sSL \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - http://localhost/api/database/1/query/1 | jq + -H "Content-Type: application/json" \ + -u "foo:bar" \ + http://<hostname>/api/database/1/query/1 | jq ``` The subset information shows the most important metadata like subset query hash and result hash (e.g. for @@ -790,19 +625,20 @@ A user wants to create a subset and export it as csv file. ```bash curl -sSL \ -X PUT \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - -d '{"persist":true}' \ - http://localhost/api/database/1/query/1 | jq + -H "Content-Type: application/json" \ + -u "foo:bar" \ + -d '{"persist": true}' \ + http://<hostname>/api/database/1/query/1 | jq ``` The subset can be exported to a `subset_export.csv` file (this also works for non-persisted subsets). ```bash curl -sSL \ - -H "Authorization: Bearer ACCESS_TOKEN" \ + -u "foo:bar" \ -H "Accept: text/csv" \ -o subset_export.csv \ - http://localhost/api/database/1/query/1 + http://<hostname>/api/database/1/query/1 ``` === "JDBC" @@ -827,56 +663,34 @@ A user wants to create a subset and export it as csv file. === "Python" - Obtain an access token: - - ```python - import requests - - response = requests.post("http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token", json={ - "username": "foo", - "password": "bar", - "grant_type": "password", - "client_id": "dbrepo-client", - "scope": "openid", - "client_secret": "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" - }) - access_token = response.json()["access_token"] - print(access_token) - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - A subset can be created by passing a SQL query to a database where you have at least `read` access (this is the case for e.g. self-created databases). ```python - import requests - - response = requests.post("http://localhost/api/database/1/query?page=0&sort=10", headers={ - "Authorization": "Bearer " + access_token - }, json={ - "statement": "SELECT `id`,`datetime`,`level` FROM `danube_water_levels`" - }) - query_id = response.json()["id"] + from dbrepo.RestClient import RestClient + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + subset = client.execute_query(database_id=1, + query="SELECT `id`,`datetime` FROM `danube_water_levels`") + print(f"subset id: {subset.id}") ``` !!! note - An optional field `"timestamp":"2024-01-16 23:00:00"` can be provided to execute a query with a system time of + An optional field `timestamp="2024-01-16 23:00:00"` can be provided to execute a query with a system time of 2024-01-16 23:00:00 (UTC). Make yourself familiar with the concept of [System Versioned Tables](https://mariadb.com/kb/en/system-versioned-tables/) for more information. + The subset information can be shown: + ```python - import requests + from dbrepo.RestClient import RestClient - response = requests.get("http://localhost/api/database/1/query/1", headers={ - "Authorization": "Bearer " + access_token - }) - print(response.json()) + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + subset = client.get_query(database_id=1, query_id=6) + print(subset) ``` The subset information shows the most important metadata like subset query hash and result hash (e.g. for @@ -887,31 +701,21 @@ A user wants to create a subset and export it as csv file. persist it. ```python - import requests - - response = requests.put("http://localhost/api/database/1/query/1", headers={ - "Authorization": "Bearer " + access_token - }, json={ - "persist": True - }) + from dbrepo.RestClient import RestClient + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + client.update_query(database_id=1, query_id=6, persist=True) ``` The subset can be exported to a `subset_export.csv` file (this also works for non-persisted subsets). ```python - import requests + from dbrepo.RestClient import RestClient - headers = { - "Authorization": "Bearer " + access_token, - "Accept": "text/csv" - } - - with requests.Session() as s: - response = s.get("http://localhost/api/database/1/query/1", - headers=headers, stream=True) - decoded_content = response.content.decode('utf-8') - with open("subset_export.csv", "w") as f: - f.write(decoded_content) + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + client.get_query_data(database_id=1, query_id=6, file_path='subset_export.csv') ``` ## Assign Database PID @@ -997,30 +801,16 @@ A user wants to assign a persistent identifier to a database owned by them. === "Terminal" - Obtain an access token: - - ```bash - curl -sSL \ - -X POST \ - -d 'username=foo&password=bar&grant_type=password&client_id=dbrepo-client&scope=openid&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG' \ - http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token | jq .access_token - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - Create a persistent identifier for a database where you are the owner (this is the case for self-created databases) using the HTTP API: ```bash curl -sSL \ -X POST \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - -d '{"type": "database", "titles": [{"title": "Danube water level measurements", "language": "en"},{"title": "Donau Wasserstandsmessungen", "language": "de", "type": "TranslatedTitle"}], "descriptions": [{"description": "This dataset contains hourly measurements of the water level in Vienna from 1983 to 2015", "language": "en", "type": "Abstract"}], "funders": [{ "funder_name": "Austrian Science Fund", "funder_identifier": "https://doi.org/10.13039/100000001", "funder_identifier_type": "Crossref Funder ID", "scheme_uri": "http://doi.org/"}], "licenses": [{"identifier": "CC-BY-4.0", "uri": "https://creativecommons.org/licenses/by/4.0/legalcode"}], "visibility": "everyone", "publisher": "Example University", "creators": [{"firstname": "Martin", "lastname": "Weise", "affiliation": "TU Wien", "creator_name": "Weise, Martin", "name_type": "Personal", "name_identifier": "0000-0003-4216-302X", "name_identifier_scheme": "ORCID", "affiliation_identifier": "https://ror.org/04d836q62", "affiliation_identifier_scheme": "ROR"}], "database_id": 1, "publication_day": 16, "publication_month": 1, "publication_year": 2024, "related_identifiers": [{"value": "10.5334/dsj-2022-004", "type": "DOI", "relation": "Cites"}]}' \ - http://localhost/api/identifier | jq .id + -H "Content-Type: application/json" \ + -u "foo:bar" \ + -d '{"type": "database", "titles": [{"title": "Danube water level measurements", "language": "en"},{"title": "Donau Wasserstandsmessungen", "language": "de", "type": "TranslatedTitle"}], "descriptions": [{"description": "This dataset contains hourly measurements of the water level in Vienna from 1983 to 2015", "language": "en", "type": "Abstract"}], "funders": [{ "funder_name": "Austrian Science Fund", "funder_identifier": "https://doi.org/10.13039/100000001", "funder_identifier_type": "Crossref Funder ID", "scheme_uri": "http://doi.org/"}], "licenses": [{"identifier": "CC-BY-4.0", "uri": "https://creativecommons.org/licenses/by/4.0/legalcode"}], "publisher": "Example University", "creators": [{"firstname": "Martin", "lastname": "Weise", "affiliation": "TU Wien", "creator_name": "Weise, Martin", "name_type": "Personal", "name_identifier": "0000-0003-4216-302X", "name_identifier_scheme": "ORCID", "affiliation_identifier": "https://ror.org/04d836q62", "affiliation_identifier_scheme": "ROR"}], "database_id": 1, "publication_day": 16, "publication_month": 1, "publication_year": 2024, "related_identifiers": [{"value": "10.5334/dsj-2022-004", "type": "DOI", "relation": "Cites"}]}' \ + http://<hostname>/api/identifier | jq .id ``` === "JDBC" @@ -1044,99 +834,39 @@ A user wants to assign a persistent identifier to a database owned by them. === "Python" - Obtain an access token: - - ```python - import requests - - response = requests.post("http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token", json={ - "username": "foo", - "password": "bar", - "grant_type": "password", - "client_id": "dbrepo-client", - "scope": "openid", - "client_secret": "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" - }) - access_token = response.json()["access_token"] - print(access_token) - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - Create a persistent identifier for a database where you are the owner (this is the case for self-created databases) using the HTTP API: ```python - import requests - - response = requests.post("http://localhost/api/database/1/query/1", headers={ - "Authorization": "Bearer " + access_token - }, json={ - "type": "database", - "titles": [ - { - "title": "Danube water level measurements", - "language": "en" - }, - { - "title": "Donau Wasserstandsmessungen", - "language": "de", - "type": "TranslatedTitle" - } - ], - "descriptions": [ - { - "description": "This dataset contains hourly measurements of the water level in Vienna from 1983 to 2015", - "language": "en", - "type": "Abstract" - } - ], - "funders": [ - { - "funder_name": "Austrian Science Fund", - "funder_identifier": "https://doi.org/10.13039/100000001", - "funder_identifier_type": "Crossref Funder ID", - "scheme_uri": "http://doi.org/" - } - ], - "licenses": [ - { - "identifier": "CC-BY-4.0", - "uri": "https://creativecommons.org/licenses/by/4.0/legalcode" - } - ], - "visibility": "everyone", - "publisher": "Example University", - "creators": [ - { - "firstname": "Martin", - "lastname": "Weise", - "affiliation": "TU Wien", - "creator_name": "Weise, Martin", - "name_type": "Personal", - "name_identifier": "0000-0003-4216-302X", - "name_identifier_scheme": "ORCID", - "affiliation_identifier": "https://ror.org/04d836q62", - "affiliation_identifier_scheme": "ROR" - } - ], - "database_id": 1, - "publication_day": 16, - "publication_month": 1, - "publication_year": 2024, - "related_identifiers": [ - { - "value": "10.5334/dsj-2022-004", - "type": "DOI", - "relation": "Cites" - } - ] - }) - pid = response.json()["id"] + from dbrepo.RestClient import RestClient + from python.dbrepo.api.dto import IdentifierType, CreateIdentifierFunder, CreateIdentifierDescription, \ + CreateIdentifierTitle, Identifier, CreateIdentifierCreator, CreateRelatedIdentifier, RelatedIdentifierType, \ + RelatedIdentifierRelation + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + identifier = client.create_identifier( + database_id=1, + type=IdentifierType.DATABASE, + creators=[CreateIdentifierCreator( + creator_name="Weise, Martin", + name_identifier="https://orcid.org/0000-0003-4216-302X")], + titles=[CreateIdentifierTitle( + title="Danube river water measurements")], + descriptions=[CreateIdentifierDescription( + description="This dataset contains hourly measurements of the water \ + level in Vienna from 1983 to 2015")], + funders=[CreateIdentifierFunder( + funder_name="Austrian Science Fund", + funder_identifier="https://doi.org/10.13039/100000001")], + licenses=[Identifier(identifier="CC-BY-4.0")], + publisher="TU Wien", + publication_year=2024, + related_identifiers=[CreateRelatedIdentifier( + value="https://doi.org/10.5334/dsj-2022-004", + type=RelatedIdentifierType.DOI, + relation=RelatedIdentifierRelation.CITES)]) + print(f"identifier id: {identifier.id}") ``` ## Private Database & Access @@ -1169,30 +899,16 @@ A user wants a public database to be private and only give specific users access === "Terminal" - Obtain an access token: - - ```bash - curl -sSL \ - -X POST \ - -d 'username=foo&password=bar&grant_type=password&client_id=dbrepo-client&scope=openid&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG' \ - http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token | jq .access_token - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - To change the visibility of a database where you are the owner (this is the case for self-created databases), send a request to the HTTP API: ```bash curl -sSL \ -X PUT \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - -d '{"is_public":true}' \ - http://localhost/api/database/1/visibility + -H "Content-Type: application/json" \ + -u "foo:bar" \ + -d '{"is_public": true}' \ + http://<hostname>/api/database/1/visibility ``` To give a user (with id `e9bf38a0-a254-4040-87e3-92e0f09e29c8` access to this database (e.g. read access), update @@ -1201,9 +917,10 @@ A user wants a public database to be private and only give specific users access ```bash curl -sSL \ -X POST \ - -H "Authorization: Bearer ACCESS_TOKEN" \ - -d '{"type":"read"}' \ - http://localhost/api/database/1/access/e9bf38a0-a254-4040-87e3-92e0f09e29c8 + -H "Content-Type: application/json" \ + -u "foo:bar" \ + -d '{"type": "read"}' \ + http://<hostname>/api/database/1/access/e9bf38a0-a254-4040-87e3-92e0f09e29c8 ``` In case the user already has access, use the method `PUT`. @@ -1230,49 +947,29 @@ A user wants a public database to be private and only give specific users access === "Python" - Obtain an access token: - - ```python - import requests - - response = requests.post("http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token", json={ - "username": "foo", - "password": "bar", - "grant_type": "password", - "client_id": "dbrepo-client", - "scope": "openid", - "client_secret": "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" - }) - access_token = response.json()["access_token"] - print(access_token) - ``` - - !!! note - - Please note that the `client_secret` is different for your DBRepo instance. This is a default client secret that - likely has been replaced. Please contact your DBRepo administrator to get the `client_secret` for your instance. - Similar you need to replace `localhost` with your actual DBRepo instance hostname, e.g. `test.dbrepo.tuwien.ac.at`. - To change the visibility of a database where you are the owner (this is the case for self-created databases), send a request to the HTTP API: ```python - requests.put("http://localhost/api/database/1/visibility", headers={ - "Authorization": "Bearer " + access_token - }, json={ - "is_public": True - }) + from dbrepo.RestClient import RestClient + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + client.update_database_visibility(database_id=1, is_public=False) ``` To give a user (with id `e9bf38a0-a254-4040-87e3-92e0f09e29c8` access to this database (e.g. read access), update their access using the HTTP API: ```python - requests.post("http://localhost/api/database/1/access/e9bf38a0-a254-4040-87e3-92e0f09e29c8", headers={ - "Authorization": "Bearer " + access_token - }, json={ - "type": "read" - }) + from dbrepo.RestClient import RestClient + from python.dbrepo.api.dto import AccessType + + client = RestClient(endpoint="http://<hostname>", username="foo", + password="bar") + client.create_database_access(database_id=1, + type=AccessType.READ, + user_id="e9bf38a0-a254-4040-87e3-92e0f09e29c8") ``` - In case the user already has access, use the method `put`. \ No newline at end of file + In case the user already has access, use the method `update_database_access` or revoke `delete_database_access`. diff --git a/.docs/usage-python.md b/.docs/usage-python.md new file mode 100644 index 0000000000..ce83d6304f --- /dev/null +++ b/.docs/usage-python.md @@ -0,0 +1,124 @@ +--- +author: Martin Weise +--- + +# Python Library + +## tl;dr + +!!! debug "Debug Information" + + PyPI: [`dbrepo`](https://pypi.org/project/dbrepo/) + + * Full module documentation <a href="../sphinx" target="_blank">:fontawesome-solid-square-up-right: view online</a> + +## Installing + +:octicons-tag-16:{ title="Minimum version" } 1.4.2 + +```console +$ python -m pip install dbrepo +``` + +To use DBRepo in your Jupyter notebook, install the `dbrepo` library` directly in a code cell and type: + +```jupyter +!pip install dbrepo +``` + +This package supports Python 3.11+. + +## Quickstart + +Create a table and import a .csv file from your computer. + +```python +from dbrepo.RestClient import RestClient +from dbrepo.api.dto import CreateTableColumn, ColumnType, CreateTableConstraints + +client = RestClient(endpoint='https://test.dbrepo.tuwien.ac.at', username="foo", + password="bar") + +# analyse csv +analysis = client.analyse_datatypes(file_path="sensor.csv", separator=",") +print(f"Analysis result: {analysis}") +# -> columns=(date=date, precipitation=decimal, lat=decimal, lng=decimal), separator=, +# line_termination=\n + +# 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)]) +print(f"Create table result {table}") +# -> (id=1, internal_name=sensor_data, ...) + +client.import_table_data(database_id=1, table_id=1, file_path="sensor.csv", separator=",", + skip_lines=1, line_encoding="\n") +print(f"Finished.") +``` + +The library is well-documented, please see the [full documentation](../sphinx) or +the [PyPI page](https://pypi.org/project/dbrepo/). + +## Supported Features & Best-Practices + +- Manage user account ([docs](../usage-overview/#create-user-account)) +- Manage databases ([docs](../usage-overview/#create-database)) +- Manage database access & visibility ([docs](../usage-overview/#private-database-access)) +- Import dataset ([docs](../usage-overview/#private-database-access)) +- Create persistent identifiers ([docs](../usage-overview/#assign-database-pid)) +- Execute queries ([docs](../usage-overview/#export-subset)) +- Get data from tables/views/subsets + +## Secrets + +It is not recommended to store credentials directly in the notebook as they will be versioned with git, etc. Use +environment variables instead: + +```properties title=".env" +DBREPO_ENDPOINT=https://test.dbrepo.tuwien.ac.at +DBREPO_USERNAME=foo +DBREPO_PASSWORD=bar +DBREPO_SECURE=True +``` + +Then use the default constructor of the `RestClient` to e.g. analyse a CSV. Your secrets are automatically passed: + +```python title="analysis.py" +from dbrepo.RestClient import RestClient + +client = RestClient() +analysis = client.analyse_datatypes(file_path="sensor.csv", separator=",") +``` + +## Future + +- Searching + +## Links + +This information is also mirrored on [PyPI](https://pypi.org/project/dbrepo/). \ No newline at end of file diff --git a/.docs/usage-search.md b/.docs/usage-search.md deleted file mode 100644 index c21ae72c32..0000000000 --- a/.docs/usage-search.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -author: Martin Weise ---- - -# Search Service - -The Search Service connects to the [Search Database](../system-databases-search/). - -!!! note "This section will be expanded" \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d45c437c6d..b8385ade8a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,12 +18,22 @@ cache: - .m2/ stages: + - lint - build - test - docs - release - scan +lint-yaml: + image: bash:5.2-alpine3.19 + stage: build + script: + - "apk add yq" + - "yq '.services.[] | .environment' docker-compose.yml > ./doc.txt" + - "yq '.services.[] | .environment' docker-compose.prod.yml > ./other.txt" + - "cmp --silent ./doc.txt ./other.txt" + build-metadata-service: image: maven:3-openjdk-17 stage: build @@ -45,6 +55,18 @@ build-analyse-service: - "pip install pipenv" - "pipenv install gunicorn && pipenv install --dev --system --deploy" +build-lib: + image: python:3.11-slim + stage: build + except: + refs: + - /^release-.*/ + variables: + PIPENV_PIPFILE: "./lib/python/Pipfile" + script: + - "pip install pipenv" + - "pipenv install gunicorn && pipenv install --dev --system --deploy" + build-data-service: image: maven:3-openjdk-17 stage: build @@ -174,6 +196,31 @@ test-analyse-service: junit: ./dbrepo-analyse-service/report.xml coverage: '/TOTAL.*?([0-9]{1,3})%/' +test-lib: + image: python:3.11-slim + stage: test + except: + refs: + - /^release-.*/ + variables: + PIPENV_PIPFILE: "./lib/python/Pipfile" + needs: + - build-lib + script: + - "pip install pipenv" + - "pipenv install gunicorn && pipenv install --dev --system --deploy" + - cd ./lib/python/ && coverage run -m pytest tests/test_database.py --junitxml=report.xml && coverage html --omit="test/*" && coverage report --omit="test/*" > ./coverage.txt + - "cat ./coverage.txt | grep -o 'TOTAL[^%]*%'" + artifacts: + when: always + paths: + - ./lib/python/report.xml + - ./lib/python/coverage.txt + expire_in: 1 days + reports: + junit: ./lib/python/report.xml + coverage: '/TOTAL.*?([0-9]{1,3})%/' + scan-analyse-service: image: bitnami/trivy:latest stage: scan @@ -514,3 +561,22 @@ release-docs: - tar czfv ./final.tar.gz ./final - "scp -oHostKeyAlgorithms=+ssh-rsa -oPubkeyAcceptedAlgorithms=+ssh-rsa final.tar.gz $CI_DOC_USER@$CI_DOC_IP:final.tar.gz" - "ssh -oHostKeyAlgorithms=+ssh-rsa -oPubkeyAcceptedAlgorithms=+ssh-rsa $CI_DOC_USER@$CI_DOC_IP 'rm -rf /system/user/ifs/infrastructures/public_html/dbrepo/*; tar xzfv ./final.tar.gz; rm -f ./final.tar.gz; cp -r ./final/* /system/user/ifs/infrastructures/public_html/dbrepo; rm -rf ./final'" + +release-libs: + stage: lint + image: docker.io/python:3.11-alpine + only: + refs: + - /^release-[0-9]+.*/ + variables: + PIPENV_PIPFILE: "./dbrepo-analyse-service/Pipfile" + script: + - apk add sed + - pip install pipenv + - pipenv install gunicorn && pipenv install --dev --system --deploy + - pip install twine build + - 'sed -i -e "s/__APPVERSION__/${APP_VERSION}rc18/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 diff --git a/Makefile b/Makefile index e931d4de5b..a623f1ba92 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,9 @@ build-metadata-service: build-analyse-service: bash ./dbrepo-analyse-service/build.sh +build-lib-python: + bash ./lib/python/build.sh + build-docker: bash ./bin/build-docker.sh @@ -131,7 +134,7 @@ release-storage-service-init: tag-storage-service-init docker push "${REPOSITORY_1_URL}/storage-service-init:${TAG}" docker push "${REPOSITORY_2_URL}/storage-service-init:${TAG}" -test-backend: test-metadata-service test-analyse-service test-data-service +test-backend: test-metadata-service test-analyse-service test-data-service test-lib-python test-data-service: build-data-service mvn -f ./dbrepo-data-service/pom.xml clean test verify @@ -142,6 +145,9 @@ test-metadata-service: build-metadata-service test-analyse-service: build-analyse-service bash ./dbrepo-analyse-service/test.sh +test-lib-python: build-lib-python + bash ./lib/python/test.sh + scan: scan-analyse-service scan-authentication-service scan-broker-service scan-gateway-service scan-metadata-db scan-metadata-service scan-search-db scan-ui scan-data-service scan-data-db scan-search-dashboard scan-search-service scan-analyse-service: @@ -226,4 +232,5 @@ build-api: bash .docs/.swagger/swagger-generate.sh docs: - bash .docs/build-website.sh \ No newline at end of file + bash .docs/build-website.sh + bash ./lib/python/build-website.sh \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index ae6ef935a1..0000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,1003 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "f1691729be450945956d143e93645380de19a546125e08a29e5eccbf54d97e1a" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.11" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "babel": { - "hashes": [ - "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", - "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287" - ], - "markers": "python_version >= '3.7'", - "version": "==2.14.0" - }, - "beautifulsoup4": { - "hashes": [ - "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", - "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==4.12.3" - }, - "brotli": { - "hashes": [ - "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", - "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", - "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", - "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", - "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", - "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", - "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", - "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", - "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", - "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", - "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", - "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", - "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", - "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", - "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", - "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", - "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", - "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", - "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", - "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", - "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", - "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", - "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", - "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", - "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", - "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", - "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", - "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", - "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", - "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", - "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", - "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", - "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", - "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", - "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", - "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", - "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", - "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", - "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", - "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", - "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", - "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", - "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", - "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", - "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", - "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", - "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", - "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", - "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", - "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", - "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", - "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", - "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", - "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", - "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", - "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", - "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", - "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", - "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", - "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", - "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", - "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", - "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", - "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", - "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", - "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", - "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", - "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", - "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", - "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", - "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", - "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", - "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", - "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", - "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", - "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", - "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", - "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", - "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", - "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", - "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", - "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2", - "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064" - ], - "version": "==1.1.0" - }, - "certifi": { - "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" - ], - "markers": "python_version >= '3.6'", - "version": "==2023.11.17" - }, - "cffi": { - "hashes": [ - "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", - "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", - "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", - "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", - "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", - "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", - "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", - "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", - "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", - "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", - "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", - "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", - "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", - "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", - "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", - "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", - "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", - "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", - "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", - "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", - "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", - "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", - "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", - "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", - "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", - "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", - "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", - "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", - "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", - "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", - "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", - "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", - "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", - "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", - "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", - "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", - "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", - "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", - "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", - "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", - "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", - "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", - "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", - "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", - "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", - "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", - "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", - "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", - "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", - "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", - "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", - "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" - ], - "markers": "python_version >= '3.8'", - "version": "==1.16.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "click": { - "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.7" - }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.4.6" - }, - "cssselect2": { - "hashes": [ - "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a", - "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969" - ], - "markers": "python_version >= '3.7'", - "version": "==0.7.0" - }, - "fonttools": { - "extras": [ - "woff" - ], - "hashes": [ - "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e", - "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37", - "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac", - "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae", - "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b", - "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc", - "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b", - "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07", - "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70", - "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71", - "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df", - "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6", - "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1", - "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670", - "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7", - "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6", - "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635", - "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899", - "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50", - "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9", - "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085", - "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb", - "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c", - "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3", - "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184", - "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8", - "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506", - "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c", - "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c", - "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b", - "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0", - "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c", - "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa", - "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f", - "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4", - "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c", - "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1", - "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946", - "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d", - "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952", - "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703", - "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8" - ], - "markers": "python_version >= '3.8'", - "version": "==4.47.2" - }, - "ghp-import": { - "hashes": [ - "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", - "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343" - ], - "version": "==2.1.0" - }, - "html5lib": { - "hashes": [ - "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", - "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.1" - }, - "idna": { - "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" - ], - "markers": "python_version >= '3.5'", - "version": "==3.6" - }, - "jinja2": { - "hashes": [ - "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", - "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.3" - }, - "libsass": { - "hashes": [ - "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4", - "sha256:34cae047cbbfc4ffa832a61cbb110f3c95f5471c6170c842d3fed161e40814dc", - "sha256:4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306", - "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880", - "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c", - "sha256:ea97d1b45cdc2fc3590cb9d7b60f1d8915d3ce17a98c1f2d4dd47ee0d9c68ce6" - ], - "markers": "python_version >= '3.8'", - "version": "==0.23.0" - }, - "markdown": { - "hashes": [ - "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd", - "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8" - ], - "markers": "python_version >= '3.8'", - "version": "==3.5.2" - }, - "markupsafe": { - "hashes": [ - "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69", - "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0", - "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d", - "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec", - "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5", - "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411", - "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3", - "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74", - "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0", - "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949", - "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d", - "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279", - "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f", - "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6", - "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc", - "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e", - "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954", - "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656", - "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc", - "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518", - "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56", - "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc", - "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa", - "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565", - "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4", - "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb", - "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250", - "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4", - "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959", - "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc", - "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474", - "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863", - "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8", - "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f", - "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2", - "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e", - "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e", - "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb", - "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f", - "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a", - "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26", - "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d", - "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2", - "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131", - "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789", - "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6", - "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a", - "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858", - "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e", - "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb", - "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e", - "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84", - "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7", - "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea", - "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b", - "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6", - "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475", - "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74", - "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a", - "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.4" - }, - "mergedeep": { - "hashes": [ - "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", - "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307" - ], - "markers": "python_version >= '3.6'", - "version": "==1.3.4" - }, - "mkdocs": { - "hashes": [ - "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1", - "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2" - ], - "index": "pypi", - "version": "==1.5.3" - }, - "mkdocs-material": { - "hashes": [ - "sha256:4480d9580faf42fed0123d0465502bfc1c0c239ecc9c4d66159cf0459ea1b4ae", - "sha256:ac50b2431a79a3b160fdefbba37c9132485f1a69166aba115ad49fafdbbbc5df" - ], - "index": "pypi", - "version": "==9.5.5" - }, - "mkdocs-material-extensions": { - "hashes": [ - "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", - "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31" - ], - "index": "pypi", - "version": "==1.3.1" - }, - "mkdocs-with-pdf": { - "hashes": [ - "sha256:002d76417b5cc584effdfdb6ec8d073266a308a85680c430562e97f00b886e49", - "sha256:bda3375d7040d1b8871da17c6d71ea736bdca6c669608f28ed62771031d2e0c6" - ], - "index": "pypi", - "version": "==0.9.3" - }, - "packaging": { - "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2" - }, - "paginate": { - "hashes": [ - "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d" - ], - "version": "==0.5.6" - }, - "pathspec": { - "hashes": [ - "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" - ], - "markers": "python_version >= '3.8'", - "version": "==0.12.1" - }, - "pillow": { - "hashes": [ - "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", - "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", - "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", - "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", - "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", - "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", - "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", - "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", - "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", - "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", - "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", - "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", - "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", - "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", - "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", - "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", - "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", - "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", - "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", - "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", - "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", - "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", - "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", - "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", - "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", - "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", - "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", - "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", - "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", - "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", - "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", - "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", - "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", - "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", - "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", - "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", - "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", - "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", - "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", - "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", - "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", - "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", - "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", - "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", - "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", - "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", - "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", - "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", - "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", - "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", - "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", - "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", - "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", - "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", - "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", - "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", - "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", - "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", - "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", - "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", - "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", - "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", - "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", - "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", - "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", - "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", - "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", - "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" - ], - "markers": "python_version >= '3.8'", - "version": "==10.2.0" - }, - "platformdirs": { - "hashes": [ - "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", - "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" - ], - "markers": "python_version >= '3.8'", - "version": "==4.1.0" - }, - "py-dotenv": { - "hashes": [ - "sha256:548c588c3b7e2ee2142b0ac97d2912d223ff38e874302426bbb6c21353817cc2" - ], - "index": "pypi", - "version": "==0.1" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "version": "==2.21" - }, - "pydyf": { - "hashes": [ - "sha256:901186a2e9f897108139426a6486f5225bdcc9b70be2ec965f25111e42f8ac5d", - "sha256:b22b1ef016141b54941ad66ed4e036a7bdff39c0b360993b283875c3f854dd9a" - ], - "markers": "python_version >= '3.7'", - "version": "==0.8.0" - }, - "pygments": { - "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" - ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" - }, - "pymdown-extensions": { - "hashes": [ - "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c", - "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb" - ], - "markers": "python_version >= '3.8'", - "version": "==10.7" - }, - "pyphen": { - "hashes": [ - "sha256:414c9355958ca3c6a3ff233f65678c245b8ecb56418fb291e2b93499d61cd510", - "sha256:596c8b3be1c1a70411ba5f6517d9ccfe3083c758ae2b94a45f2707346d8e66fa" - ], - "markers": "python_version >= '3.7'", - "version": "==0.14.0" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==2.8.2" - }, - "python-dotenv": { - "hashes": [ - "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", - "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" - ], - "index": "pypi", - "version": "==1.0.1" - }, - "pyyaml": { - "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" - }, - "pyyaml-env-tag": { - "hashes": [ - "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", - "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069" - ], - "markers": "python_version >= '3.6'", - "version": "==0.1" - }, - "regex": { - "hashes": [ - "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5", - "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770", - "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc", - "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105", - "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d", - "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b", - "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9", - "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630", - "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6", - "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c", - "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482", - "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6", - "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a", - "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80", - "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5", - "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1", - "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f", - "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf", - "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb", - "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2", - "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347", - "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20", - "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060", - "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5", - "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73", - "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f", - "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d", - "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3", - "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae", - "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4", - "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2", - "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457", - "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c", - "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4", - "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87", - "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0", - "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704", - "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f", - "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f", - "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b", - "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5", - "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923", - "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715", - "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c", - "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca", - "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1", - "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756", - "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360", - "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc", - "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445", - "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e", - "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4", - "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a", - "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8", - "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53", - "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697", - "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf", - "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a", - "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415", - "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f", - "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9", - "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400", - "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d", - "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392", - "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb", - "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd", - "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861", - "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232", - "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95", - "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7", - "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39", - "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887", - "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5", - "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39", - "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb", - "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586", - "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97", - "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423", - "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69", - "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7", - "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1", - "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7", - "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5", - "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8", - "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91", - "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590", - "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe", - "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c", - "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64", - "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd", - "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa", - "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31", - "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988" - ], - "markers": "python_version >= '3.7'", - "version": "==2023.12.25" - }, - "requests": { - "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" - ], - "index": "pypi", - "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'", - "version": "==1.16.0" - }, - "soupsieve": { - "hashes": [ - "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", - "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" - ], - "markers": "python_version >= '3.8'", - "version": "==2.5" - }, - "tinycss2": { - "hashes": [ - "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847", - "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627" - ], - "markers": "python_version >= '3.7'", - "version": "==1.2.1" - }, - "urllib3": { - "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" - ], - "markers": "python_version >= '3.8'", - "version": "==2.1.0" - }, - "watchdog": { - "hashes": [ - "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", - "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100", - "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", - "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", - "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", - "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41", - "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", - "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f", - "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c", - "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9", - "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", - "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709", - "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83", - "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", - "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", - "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", - "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", - "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", - "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", - "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674", - "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397", - "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96", - "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d", - "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", - "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", - "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", - "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33" - ], - "markers": "python_version >= '3.7'", - "version": "==3.0.0" - }, - "weasyprint": { - "hashes": [ - "sha256:0c0cdd617a78699262b80026e67fa1692e3802cfa966395436eeaf6f787dd126", - "sha256:3e98eedcc1c5a14cb310c293c6d59a479f59a13f0d705ff07106482827fa5705" - ], - "markers": "python_version >= '3.7'", - "version": "==60.2" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "zopfli": { - "hashes": [ - "sha256:0574372283befa5af98fb31407e1fe6822f2f9c437ef69e7fa260e49022d8a65", - "sha256:082f030b2b7d6d4597ac517816e499c63b92130aa8f4f74a3788ebaa5770f974", - "sha256:08d105a49576a9e629f53a710f0009c4bf0a1d8a3239a74e41d0944f26e28a61", - "sha256:09ad5f8d7e0fe1975ca6d9fd5ad61c74233ae277982d3bc8814b599bbeb92f44", - "sha256:0fbb6e7fc0da56835167e3c83a45b28e99ba14b671ecb8e51100ad03dfffc3d0", - "sha256:13d151d5c83980f384439c87a5511853890182c05d93444f3cb05e5ceed37d82", - "sha256:1c5fd29730024f5fb0e2623e3853ca422bd3cf57042389c8e0e771dc47f88084", - "sha256:1f25f1bb6440ed90a1d458772fa6ce53632f5fb61e435b12ae6b9b39af98d758", - "sha256:2073b07c3ec4fcbc895bb02565a90f9f31373233979f6be398e82eacbd1105f3", - "sha256:22b1cfc398a87754730f7e268693c8eb480cb688fd645648fda85614a8b1c08c", - "sha256:2770cf6b88e9985c79b90fd6d4c15d8dab0caa37c1c3b17773e61ce857eab586", - "sha256:27f2b58050f84fa059db7a6ec17d98b388c18f9783551e5f97605f790f25e155", - "sha256:2da6f30632cefda8ebe032fdcb69cf062f5a6435af9d32de82ccef320e0261f5", - "sha256:31c467a300ba46f55aa0ea958ea388e350eefd039cf15764bf4cd737d5eeb8a6", - "sha256:39d8a73bee07cf7f2c73e08508bf788bfdf28a527da353b5d3e2a0ee4aaf770c", - "sha256:3e4675ca4c7b1215b8a53cec1831cbdb6914f91ea2f183817a06fc7b38e27642", - "sha256:40665bf0bacc8b82652a1af4016648dd69f896afa59fc481c1d19a222aa746ea", - "sha256:40b830244e6458ef982b4a5ebb0f228986d481408bae557a95eeece2c5ede4e6", - "sha256:52438999888715a378fc6fe1477ab7813e9e9b58a27a38d2ad7be0e396b1ab2e", - "sha256:57f93802e5ddb20647747ee4039a2e18a26e91bac4c41d3d75a2b2c97f270549", - "sha256:5e52aaab3a93470cf0ff2bb2135a8628dda7b70f675c46f35b6a1b30e8e482f4", - "sha256:6020a3533c6c7be09db9e59c2a8f3f894bf5d8e95cc01890d82114c923317c57", - "sha256:61a2fcc624e8b038d4fca84ba927dc3f31df53a7284692d46aa44d16fb3f47b2", - "sha256:61abe5f11400f9c6b22be578091e28dfb9f1a61efaaeaa2da66138b03ee93072", - "sha256:6225bbc33c4f803cdc1e71e3028af96dd0e1ed3cf061780d1bf05648df616a05", - "sha256:711d4fde9cb99e1a9158978e9d1624a37cdd170ff057f6340059514fcf38e808", - "sha256:72349c78da402e6784bd9c5f4aff5cc7017bd969016ec07b656722f7f29fc975", - "sha256:7463b42a2cee33f0a018bf8f1304da2379d6cb8111aa4e04d8f8590d0f1099e1", - "sha256:7599ce108386d91a402969cba4f17247e33a594b21cbd662e008414ccb0b4cf7", - "sha256:7769f6ca73f37dff92159127bd25b0cc7d81d3feb819d355dc7ac01ad05c673d", - "sha256:78022777139ac973286219e9e085d9496fb6c935502d93a52bd1bed01dfc2002", - "sha256:7bc89b71d1c4677f708cc162f40a4560f78f5f4c6aa6d884b423df7d38e8ba0b", - "sha256:7ddcbc258bb5c07ebb7f6ee64c46d4e35c39c6abba2b3dfa72c0ea4daf9e65fc", - "sha256:7ebb4e1b0f102d431830151041777c55700d12afd1e5adb5bcbce72037c1a10e", - "sha256:81d61eba5a8e221b297a1dd27f1dae2785a14a5524cc1e144da53705cf90d5c4", - "sha256:8293062567917201609b28b865289d5ddee55030c779fa9264caae4cc2e00fb3", - "sha256:84321886cf3e80e086e0f6f7b765975343aafa61165315bb8db514d0bec2d887", - "sha256:92ca61eaa1df774908c173683e23c512189bf791a7ebb49fac61324658cff490", - "sha256:975d45745cf6c3e3b61127e0140dcf145fa515f2021f669bd82768937b7bb1fb", - "sha256:978395a4ce5cc46db29a36cdb80549b564dc7706237abaca5aac328dd5842f65", - "sha256:97d2f993142fed4f9c11c1766eb53409efe7298c755cf4599c171bfedcbaddae", - "sha256:9dcf7af42c11b3cf5d3fbf665799e10f54f66caea2020fe304602df83b9a1a69", - "sha256:ad2a98890045d13b0cdc93c1637990c211dc877493469afc61a097a00a70cf22", - "sha256:ae890df6e5f1e8fa0697cafd848826decce0ac53e54e5a018fd97775e3a354c0", - "sha256:b30a922b9d73f22da2b589b35e594dcc6d144eb38ad890c542f2b92902ba9892", - "sha256:c1afe5ba0d957e462afbd3da116ac1a2a6d23e8a94436a95b692c5c324694a16", - "sha256:c3c61787a90439cf68f751b2a1ab789b0805876c0cd9b58398adc212d1eeace5", - "sha256:c6555293e42e7a9154940bb18613de2abce21a855780baff8a6c372e395c59b3", - "sha256:ca9a6df3d11c2f8f0356c141523c4914a2850dd39fc213d968c0272db635eea9", - "sha256:d0a8e556916088fadb098ddb6eed034d5c2df3b8fba7f2488e87e8c224002eca", - "sha256:d40373db61883f6fc8b7040c9196a16f737e3063632afd15e8b3f25e871a30e8", - "sha256:dbc9841bedd736041eb5e6982cd92da93bee145745f5422f3795f6f258cdc6ef", - "sha256:dc59299eda2aaf57f0ee5c4b42ada0b80e3dc4c09c5bdda8ee9ae5cf93fafa9e", - "sha256:deffa15253a43a597e8ebf82ca1908bd70b3bf899da163b017d49ddd5e12732a", - "sha256:e4068d4d35b2e63898d22e1b7777d986b8f5d61fe83a77973730ce9cff1b4ba1", - "sha256:e5f62ca9a947f09f531c721e2a3f2e0094523436b8eb5df18d71245c1924f89a", - "sha256:eef08c02295bb99c7fdca380c52e5454fa1c08025fb0bea2c7ae6c0e1e9c034b", - "sha256:f07997453e7777e19ef0a2445cc1b90e1bb90c623dd77554325932dea6350fee", - "sha256:f48de4818c10c539fdd01276512043ae4ae738e0301e9cace1dd38f4bcffad6a", - "sha256:f69b161b4d49e256ab285c6c6ee1cf217fda864a9b175d24fa0a0b8c2de9ff13", - "sha256:ff86a2cd6b9864027861a129d6d73231b6d463f0d364ca0fdca4492390357cba" - ], - "version": "==0.2.3" - } - }, - "develop": {} -} diff --git a/dbrepo-analyse-service/.gitignore b/dbrepo-analyse-service/.gitignore index 612cf05985..eb26996516 100644 --- a/dbrepo-analyse-service/.gitignore +++ b/dbrepo-analyse-service/.gitignore @@ -6,7 +6,6 @@ __pycache__ .DS_Store # Environment -.env .flaskenv *.pyc *.pyo diff --git a/dbrepo-analyse-service/Pipfile b/dbrepo-analyse-service/Pipfile index 56be719598..33d0cd74fb 100644 --- a/dbrepo-analyse-service/Pipfile +++ b/dbrepo-analyse-service/Pipfile @@ -20,6 +20,8 @@ minio = "*" flask-sqlalchemy = "*" opensearch-py = "*" pymysql = "*" +dataclasses = "*" +dataclasses-json = "*" [dev-packages] coverage = "*" diff --git a/dbrepo-analyse-service/Pipfile.lock b/dbrepo-analyse-service/Pipfile.lock index 4c071adaea..8c747a022c 100644 --- a/dbrepo-analyse-service/Pipfile.lock +++ b/dbrepo-analyse-service/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "652c3e1fcfa9736a09b28e0fb5df79dddb7b70e0107e2079aa9cda546b70606c" + "sha256": "bec6f97fa1f79cd9ecaf77da235e2182f027fe913a79f7020585cc7e5507058a" }, "pipfile-spec": 6, "requires": { @@ -69,19 +69,20 @@ }, "boto3": { "hashes": [ - "sha256:004e67b078be58d34469406f93cc8b95bc43becef4bbe44523a0b8e51f84c668", - "sha256:162edf182e53c198137a28432a626dba103f787a8f5000ed4758b73ccd203fa0" + "sha256:00a7cff4887e8a46c8b2ce438f33d5f87cf7812f303227adc0266f28338af6d5", + "sha256:14f1e23b3f83ec365628a6ef849f1038b4c7338c4fabff159007c711b8147efc" ], "index": "pypi", - "version": "==1.34.59" + "markers": "python_version >= '3.8'", + "version": "==1.34.68" }, "botocore": { "hashes": [ - "sha256:24edb4d21d7c97dea0c6c4a80d36b3809b1443a30b0bd5e317d6c319dfac823f", - "sha256:4bc112dafb1679ab571117593f7656604726a3da0e5ae5bad00ea772fa40e75c" + "sha256:3ad0ec67f78beecc039c3c31c93a83181e30b6f789261bdbb9f5c8e8dc551812", + "sha256:e7ae9d69cc3e7b31d926e6a1a9ae673ba02da263e35cf12ff2bae35a21755cc6" ], "markers": "python_version >= '3.8'", - "version": "==1.34.59" + "version": "==1.34.68" }, "certifi": { "hashes": [ @@ -261,12 +262,30 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "dataclasses": { + "hashes": [ + "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", + "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" + ], + "index": "pypi", + "version": "==0.6" + }, + "dataclasses-json": { + "hashes": [ + "sha256:73696ebf24936560cca79a2430cbc4f3dd23ac7bf46ed17f38e5e5e7657a6377", + "sha256:f90578b8a3177f7552f4e1a6e535e84293cd5da421fcce0642d49c0d7bdf8df2" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==0.6.4" + }, "exceptiongroup": { "hashes": [ "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.2.0" }, "flasgger": { @@ -278,11 +297,12 @@ }, "flask": { "hashes": [ - "sha256:3232e0e9c850d781933cf0207523d1ece087eb8d87b23777ae38456e2fbe7c6e", - "sha256:822c03f4b799204250a7ee84b1eddc40665395333973dfb9deebfe425fefcb7d" + "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc", + "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b" ], "index": "pypi", - "version": "==3.0.2" + "markers": "python_version >= '3.8'", + "version": "==2.3.3" }, "flask-cors": { "hashes": [ @@ -298,6 +318,7 @@ "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.1.1" }, "gevent": { @@ -345,6 +366,7 @@ "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==24.2.1" }, "greenlet": { @@ -409,6 +431,7 @@ "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.0.3" }, "gunicorn": { @@ -417,6 +440,7 @@ "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==21.2.0" }, "html5lib": { @@ -437,11 +461,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792", - "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100" + "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2" ], "markers": "python_version < '3.10'", - "version": "==7.0.2" + "version": "==7.1.0" }, "itsdangerous": { "hashes": [ @@ -639,6 +663,14 @@ "markers": "python_version >= '3.7'", "version": "==2.1.5" }, + "marshmallow": { + "hashes": [ + "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3", + "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633" + ], + "markers": "python_version >= '3.8'", + "version": "==3.21.1" + }, "messytables": { "hashes": [ "sha256:227a5aac364919a7d3faa6ce04027fcbd03e041efcd3d57fabb1d1067591a2cd" @@ -662,6 +694,14 @@ "markers": "python_version >= '3.7'", "version": "==3.0.2" }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, "numpy": { "hashes": [ "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", @@ -702,6 +742,7 @@ "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" ], "index": "pypi", + "markers": "python_version >= '3.9'", "version": "==1.26.4" }, "opensearch-py": { @@ -710,15 +751,16 @@ "sha256:7867319132133e2974c09f76a54eb1d502b989229be52da583d93ddc743ea111" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", "version": "==2.4.2" }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pandas": { "hashes": [ @@ -753,6 +795,7 @@ "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df" ], "index": "pypi", + "markers": "python_version >= '3.9'", "version": "==2.2.1" }, "prometheus-client": { @@ -822,6 +865,7 @@ "sha256:8969ec6d763c856f7073c4c64662882675702efcb114b4bcbb955aea3a069fa7" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.1.0" }, "python-dateutil": { @@ -906,11 +950,11 @@ }, "referencing": { "hashes": [ - "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5", - "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7" + "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844", + "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4" ], "markers": "python_version >= '3.8'", - "version": "==0.33.0" + "version": "==0.34.0" }, "requests": { "hashes": [ @@ -1027,19 +1071,19 @@ }, "s3transfer": { "hashes": [ - "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", - "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" + "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19", + "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d" ], "markers": "python_version >= '3.8'", - "version": "==0.10.0" + "version": "==0.10.1" }, "setuptools": { "hashes": [ - "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56", - "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8" + "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", + "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c" ], "markers": "python_version >= '3.8'", - "version": "==69.1.1" + "version": "==69.2.0" }, "six": { "hashes": [ @@ -1112,6 +1156,13 @@ "markers": "python_version >= '3.8'", "version": "==4.10.0" }, + "typing-inspect": { + "hashes": [ + "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", + "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" + ], + "version": "==0.9.0" + }, "tzdata": { "hashes": [ "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", @@ -1153,11 +1204,11 @@ }, "zipp": { "hashes": [ - "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", - "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" + "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", + "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" ], "markers": "python_version >= '3.8'", - "version": "==3.17.0" + "version": "==3.18.1" }, "zope.event": { "hashes": [ @@ -1410,61 +1461,62 @@ }, "coverage": { "hashes": [ - "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa", - "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003", - "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f", - "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c", - "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e", - "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0", - "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9", - "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52", - "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e", - "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454", - "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0", - "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079", - "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352", - "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f", - "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30", - "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe", - "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113", - "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765", - "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc", - "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e", - "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501", - "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7", - "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2", - "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f", - "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4", - "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524", - "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c", - "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51", - "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840", - "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6", - "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee", - "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e", - "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45", - "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba", - "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d", - "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3", - "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10", - "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e", - "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb", - "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9", - "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a", - "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47", - "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1", - "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3", - "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914", - "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328", - "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6", - "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d", - "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0", - "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94", - "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc", - "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2" + "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", + "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", + "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", + "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", + "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", + "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", + "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", + "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", + "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", + "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", + "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", + "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", + "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", + "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", + "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", + "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", + "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", + "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", + "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", + "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", + "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", + "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", + "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", + "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", + "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", + "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", + "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", + "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", + "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", + "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", + "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", + "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", + "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", + "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", + "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", + "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", + "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", + "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", + "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", + "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", + "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", + "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", + "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", + "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", + "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", + "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", + "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", + "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", + "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", + "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", + "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", + "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" ], "index": "pypi", - "version": "==7.4.3" + "markers": "python_version >= '3.8'", + "version": "==7.4.4" }, "docker": { "hashes": [ @@ -1480,6 +1532,7 @@ "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.2.0" }, "greenlet": { @@ -1544,6 +1597,7 @@ "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.0.3" }, "idna": { @@ -1576,15 +1630,16 @@ "sha256:7867319132133e2974c09f76a54eb1d502b989229be52da583d93ddc743ea111" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", "version": "==2.4.2" }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pluggy": { "hashes": [ @@ -1645,15 +1700,17 @@ "sha256:8969ec6d763c856f7073c4c64662882675702efcb114b4bcbb955aea3a069fa7" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.1.0" }, "pytest": { "hashes": [ - "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd", - "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096" + "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", + "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044" ], "index": "pypi", - "version": "==8.0.2" + "markers": "python_version >= '3.8'", + "version": "==8.1.1" }, "python-dateutil": { "hashes": [ @@ -1746,6 +1803,7 @@ "sha256:54d330d085c0a11fc5da0b001af87aec4dd3e814104376bf7513e8646c77442a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.0.1rc1" }, "testcontainers-mysql": { @@ -1753,6 +1811,7 @@ "sha256:d22894e0d8c7b4f7424afef99f713aa7e7a19ff987b7723aed863b9c478a2c91" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.0.1rc1" }, "testcontainers-opensearch": { @@ -1760,6 +1819,7 @@ "sha256:0bdf270b5b7f53915832f7c31dd2bd3ffdc20b534ea6b32231cc7003049bd0e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.0.1rc1" }, "tomli": { diff --git a/dbrepo-analyse-service/app.py b/dbrepo-analyse-service/app.py index 70c3d83bcf..80253a3168 100644 --- a/dbrepo-analyse-service/app.py +++ b/dbrepo-analyse-service/app.py @@ -1,3 +1,5 @@ +import dataclasses +import json import logging from _csv import Error @@ -13,6 +15,8 @@ from gevent.pywsgi import WSGIServer from opensearchpy import OpenSearch from prometheus_flask_exporter import PrometheusMetrics +from botocore.exceptions import ClientError + from determine_dt import determine_datatypes from determine_pk import determine_pk from determine_stats import determine_stats @@ -61,7 +65,6 @@ opensearch_client = OpenSearch( use_ssl=False, ) - swagger_config = { "headers": [], "specs": [ @@ -114,98 +117,82 @@ def health(): return Response(res, mimetype="application/json"), 200 -@app.route("/api/analyse/determinedt", methods=["POST"], endpoint="analyze_determinedt") -@swag_from("as-yml/determinedt.yml") -def determinedt(): - logging.debug("endpoint determine datatype, body=%s", request) - input_json = request.get_json() +@app.route("/api/analyse/datatypes", methods=["GET"], endpoint="analyze_analyse_datatypes") +@swag_from("as-yml/analyse_datatypes.yml") +def analyse_datatypes(): + filename: str = request.args.get('filename') + separator: str = request.args.get('separator') + enum: bool = request.args.get('enum', False) + enum_tol: float = request.args.get('enum_tol') + + if filename is None or separator is None: + return Response( + json.dumps({'success': False, 'message': "Missing required query parameters 'filename' and 'separator'"}), + mimetype="application/json"), 400 + try: - filename = str(input_json["filename"]) - enum = False - if "enum" in input_json: - enum = bool(input_json["enum"]) - logging.info("Enum is present in payload and set to %s", enum) - enum_tol = 0.001 - if "enum_tol" in input_json: - enum_tol = float(input_json["enum_tol"]) - logging.info( - "Enum toleration is present in payload and set to %s", enum_tol - ) - separator = None - if "separator" in input_json: - separator = str(input_json["separator"]) - logging.info("Seperator is present in payload and set to %s", separator) res = determine_datatypes(filename, enum, enum_tol, separator) logging.debug("determine datatype resulted in datatypes %s", res) - return Response(res, mimetype="application/json"), 200 + return Response(res, mimetype="application/json"), 202 except OSError as e: - logging.error("Failed to determine data types: %s", e) + logging.error(f"Failed to determine data types: {e}") res = dumps({"success": False, "message": str(e)}) - return Response(res, mimetype="application/json"), 409 - except Error as e: - logging.error("Failed to determine separator %s", e) + return Response(res, mimetype="application/json"), 400 + except ClientError as e: + logging.error(f"Failed to determine separator: {e}") res = dumps({"success": False, "message": str(e)}) - return Response(res, mimetype="application/json"), 422 + return Response(res, mimetype="application/json"), 404 except Exception as e: - logging.error("Failed to determine data types: %s", e) + logging.error(f"Failed to determine data types: {e}") res = dumps({"success": False, "message": str(e)}) return Response(res, mimetype="application/json"), 500 -@app.route("/api/analyse/determinepk", methods=["POST"], endpoint="analyze_determinepk") -@swag_from("as-yml/determinepk.yml") -def determinepk(): - logging.debug("endpoint determine primary key, body=%s", request) - input_json = request.get_json() +@app.route("/api/analyse/keys", methods=["GET"], endpoint="analyze_analyse_keys") +@swag_from("as-yml/analyse_keys.yml") +def analyse_keys(): + filename: str = request.args.get("filename") + separator: str = request.args.get('separator') + + if filename is None or separator is None: + return Response( + json.dumps({'success': False, 'message': "Missing required query parameters 'filename' and 'separator'"}), + 400) + try: - filepath = str(input_json["filepath"]) - seperator = "," - if "seperator" in input_json: - seperator = str(input_json["seperator"]) - res = determine_pk(filepath, seperator) - logging.debug("determined list of primary keys: %s", res) - return Response(res, mimetype="application/json"), 200 + res = { + 'keys': determine_pk(filename, separator) + } + logging.info(f"Determined list of primary keys: {res}") + return Response(dumps(res), mimetype="application/json"), 202 + except OSError as e: + logging.error(f"Failed to determine primary key: {e}") + res = dumps({"success": False, "message": str(e)}) + return Response(res, mimetype="application/json"), 404 except Exception as e: - logging.error("Failed to determine primary key: %s", e) + logging.error(f"Failed to determine primary key: {e}") res = dumps({"success": False, "message": str(e)}) return Response(res, mimetype="application/json"), 500 -@app.route("/api/analyse/determinestats", methods=["POST"], endpoint="analyse_determinestats") -@swag_from("as-yml/determine_stats.yml") -def determinestats(): - logging.debug( - "endpoint to determine the statistical properties, body = %s", request - ) - input_json = request.get_json() - if "filepath" not in input_json: - return {"message": "Missing 'filepath'", "status": 400}, 400 - - filepath = str(input_json["filepath"]) - separator = str(input_json.get("separator", ",")) - return determine_stats(filepath, separator) - - -@app.route("/api/analyse/determinestat", methods=["POST"], endpoint="analyse_determinestat") -@swag_from("as-yml/determine_stat.yml") -def determinestat(): - input_json = request.get_json() - - if "database_id" not in input_json: - return {"message": "Missing 'database_id'", "status": 400}, 400 - if "table_id" not in input_json: - return {"message": "Missing 'table_id'", "status": 400}, 400 - - res = determine_stats( - db, - opensearch_client, - database_id=input_json["database_id"], - table_id=input_json["table_id"], - ) - if res: - return {"message": "Analysed statistical properties.", "status": 200} - else: - return {"message": "Database or table does not exist.", "status": 400}, 400 +@app.route("/api/analyse/database/<database_id>/table/<table_id>/statistics", methods=["GET"], + endpoint="analyse_analyse_table_stat") +@swag_from("as-yml/analyse_table_stat.yml") +def analyse_table_stat(database_id: int = None, table_id: int = None): + if database_id is None: + return Response(dumps({"message": "Missing path variable 'database_id'", "status": 400}), + mimetype="application/json"), 400 + if table_id is None: + return Response(dumps({"message": "Missing path variable 'table_id'", "status": 400}), + mimetype="application/json"), 400 + + try: + res = determine_stats(db, opensearch_client, database_id=database_id, table_id=table_id) + logging.info(f"Analysed table statistics: {res}") + return Response(json.dumps(dataclasses.asdict(res)), mimetype="application/json"), 202 + except OSError: + return Response(dumps({"message": "Database or table does not exist.", "status": 404}), + mimetype="application/json"), 404 rest_server_port = 5000 diff --git a/dbrepo-analyse-service/as-yml/analyse_datatypes.yml b/dbrepo-analyse-service/as-yml/analyse_datatypes.yml new file mode 100644 index 0000000000..6ed8d9ab02 --- /dev/null +++ b/dbrepo-analyse-service/as-yml/analyse_datatypes.yml @@ -0,0 +1,103 @@ +tags: + - analyse-endpoint +summary: "Determine datatypes" +description: "This is a simple API which returns the datatypes of a (path) csv file" +consumes: + - "application/json" +produces: + - "application/json" +parameters: + - name: filename + in: query + required: true + example: filename_s3_key + schema: + type: string + - name: separator + in: query + required: true + example: "," + schema: + type: string + - name: enum + in: query + required: false + example: "false" + schema: + type: boolean + - name: enum_tol + in: query + required: false + example: "2.5" + schema: + type: float +responses: + 202: + description: Determined data types successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DataTypesDto' + 400: + description: "Failed to determine data types" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' + 404: + description: "Failed to find file in Storage Service" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' + 500: + description: "Unexpected system error" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' +components: + schemas: + DetermineDataTypesDto: + required: + - filename + - separator + type: object + properties: + enum: + type: boolean + example: false + enum_tol: + type: double + example: 0.01 + filename: + type: string + example: s3-key-from-seaweedfs + separator: + type: string + example: "," + DataTypesDto: + type: object + properties: + columns: + $ref: '#/components/schemas/SuggestedColumnDto' + line_termination: + type: string + example: "\r\n" + separator: + type: string + example: "," + SuggestedColumnDto: + type: object + properties: + column_name: + type: string + ErrorDto: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: Message \ No newline at end of file diff --git a/dbrepo-analyse-service/as-yml/analyse_keys.yml b/dbrepo-analyse-service/as-yml/analyse_keys.yml new file mode 100644 index 0000000000..5ea8c8f269 --- /dev/null +++ b/dbrepo-analyse-service/as-yml/analyse_keys.yml @@ -0,0 +1,85 @@ +tags: + - analyse-endpoint +summary: "Determine primary keys" +description: "This is a simple API which returns the primary keys + ranking of a (path) csv file" +consumes: + - "application/json" +produces: + - "application/json" +parameters: + - name: filename + in: query + required: true + example: filename_s3_key + schema: + type: string + - name: separator + in: query + required: true + example: "," + schema: + type: string +responses: + 202: + description: Determined keys successfully + content: + application/json: + schema: + $ref: '#/components/schemas/KeysDto' + 400: + description: "Failed to determine keys" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' + 404: + description: "Failed to find file in Storage Service or is empty" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' + 500: + description: "Unexpected system error" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' +components: + schemas: + KeysDto: + required: + - keys + type: object + properties: + keys: + type: array + items: + properties: + column_name: + type: integer + format: int64 + DataTypesDto: + type: object + properties: + columns: + $ref: '#/components/schemas/SuggestedColumnDto' + line_termination: + type: string + example: "\r\n" + separator: + type: string + example: "," + SuggestedColumnDto: + type: object + properties: + column_name: + type: string + ErrorDto: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: Message \ No newline at end of file diff --git a/dbrepo-analyse-service/as-yml/analyse_table_stat.yml b/dbrepo-analyse-service/as-yml/analyse_table_stat.yml new file mode 100644 index 0000000000..b7fc118ac2 --- /dev/null +++ b/dbrepo-analyse-service/as-yml/analyse_table_stat.yml @@ -0,0 +1,77 @@ +tags: + - analyse-endpoint +summary: Determine table statistics +operationId: determine_table_stat +parameters: + - name: database_id + in: path + required: true + example: 1 + schema: + type: integer + format: int64 + - name: table_id + in: path + required: true + example: 1 + schema: + type: integer + format: int64 +responses: + 202: + description: Determined statistics + content: + application/json: + schema: + $ref: '#/components/schemas/TableStats' + 400: + description: "Missing parameters" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' + 404: + description: "Table not found" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' +components: + schemas: + TableStats: + required: + - columns + type: object + properties: + columns: + type: object + properties: + column_name: + $ref: '#/components/schemas/Stats' + Stats: + type: object + properties: + val_min: + type: float + example: "0.0" + val_max: + type: float + example: "1.0" + mean: + type: float + example: "0.3" + median: + type: float + example: "0.45" + std_dev: + type: float + example: "0.12" + ErrorDto: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: Message \ No newline at end of file diff --git a/dbrepo-analyse-service/as-yml/determine_stat.yml b/dbrepo-analyse-service/as-yml/determine_stat.yml deleted file mode 100644 index 658e8af2b8..0000000000 --- a/dbrepo-analyse-service/as-yml/determine_stat.yml +++ /dev/null @@ -1,52 +0,0 @@ -tags: - - analyse-endpoint -summary: Determine statistics -operationId: determinestat -requestBody: - content: - application/json: - schema: - required: - - database_id - - table_id - type: object - properties: - database_id: - type: "integer" - example: 1 - table_id: - type: "integer" - example: 1 -responses: - "200": - description: Determined statistics - content: - application/json: - schema: - required: - - message - - status - type: object - properties: - message: - type: "string" - example: "Analysed statistical properties" - status: - type: "integer" - example: "200" - 400: - description: "Invalid input" - ontent: - application/json: - schema: - required: - - message - - status - type: object - properties: - message: - type: "string" - example: "Analysed statistical properties" - status: - type: "integer" - example: "200" diff --git a/dbrepo-analyse-service/as-yml/determine_stats.yml b/dbrepo-analyse-service/as-yml/determine_stats.yml deleted file mode 100644 index 68b71dd527..0000000000 --- a/dbrepo-analyse-service/as-yml/determine_stats.yml +++ /dev/null @@ -1,24 +0,0 @@ -tags: - - analyse-endpoint -summary: Determine statistics -operationId: determinestats -requestBody: - content: - application/json: - schema: - required: - - filepath - - separator - type: object - properties: - filepath: - type: "string" - example: "file.csv" - separator: - type: "string" - example: "," -responses: - "200": - description: Determined statistics - "400": - description: "Invalid input" diff --git a/dbrepo-analyse-service/as-yml/determinedt.yml b/dbrepo-analyse-service/as-yml/determinedt.yml deleted file mode 100644 index 8d96309747..0000000000 --- a/dbrepo-analyse-service/as-yml/determinedt.yml +++ /dev/null @@ -1,60 +0,0 @@ -tags: - - analyse-endpoint -summary: "Determine datatypes" -description: "This is a simple API which returns the datatypes of a (path) csv file" -consumes: - - "application/json" -produces: - - "application/json" -parameters: - - in: "body" - name: "body" - description: "to-do description" - required: true - schema: - type: "object" - $ref: '#/components/schemas/DetermineDataTypesDto' -responses: - 200: - description: Determined data types successfully - content: - application/json: - schema: - $ref: '#/components/schemas/DataTypesDto' - 405: - description: "Invalid input" -components: - schemas: - DetermineDataTypesDto: - type: object - properties: - enum: - type: boolean - example: false - enum_tol: - type: double - example: 0.01 - filename: - type: string - example: s3-key-from-seaweedfs - separator: - type: string - example: "," - DataTypesDto: - type: object - properties: - columns: - type: array - items: - $ref: '#/components/schemas/SuggestedColumnDto' - line_termination: - type: string - example: "\r\n" - separator: - type: string - example: "," - SuggestedColumnDto: - type: object - properties: - column_name: - type: string \ No newline at end of file diff --git a/dbrepo-analyse-service/as-yml/determinepk.yml b/dbrepo-analyse-service/as-yml/determinepk.yml deleted file mode 100644 index ca7253844d..0000000000 --- a/dbrepo-analyse-service/as-yml/determinepk.yml +++ /dev/null @@ -1,27 +0,0 @@ -tags: - - analyse-endpoint -summary: "Determine primary keys" -description: "This is a simple API which returns the primary keys + ranking of a (path) csv file" -consumes: - - "application/json" -produces: - - "application/json" -parameters: - - in: "body" - name: "body" - description: "to-do description" - required: true - schema: - type: "object" - properties: - filepath: - type: "string" - example: "/data/testdt08.csv" - seperator: - type: "string" - example: "," -responses: - 200: - description: "OK" - 405: - description: "Invalid input" \ No newline at end of file diff --git a/dbrepo-analyse-service/determine_pk.py b/dbrepo-analyse-service/determine_pk.py index 6187e0e913..1ab04df94d 100644 --- a/dbrepo-analyse-service/determine_pk.py +++ b/dbrepo-analyse-service/determine_pk.py @@ -20,8 +20,7 @@ def determine_pk(filename, separator=","): response = s3_client.get_file('dbrepo-upload', filename) stream = response['Body'] if response['ContentLength'] == 0: - logging.warning(f'Failed to determine primary key: file {filename} has empty body') - return json.dumps({'columns': [], 'separator': ','}) + raise OSError(f'Failed to determine primary key: file {filename} has empty body') sizeInKb = math.ceil(response['ContentLength'] / 1000) if sizeInKb < 400: # precise if lower than 400kB pk = {} @@ -36,10 +35,7 @@ def determine_pk(filename, separator=","): k = k + 1 csvdata = pd.read_csv(stream, sep=separator) for i in colindex: - if ( - pd.Series(csvdata.iloc[:, i]).is_unique - and pd.Series(csvdata.iloc[:, i]).notnull().values.any() - ): + if pd.Series(csvdata.iloc[:, i]).is_unique and pd.Series(csvdata.iloc[:, i]).notnull().values.any(): j = j + 1 pk.update({list(colnames)[i]: j}) else: # stochastic pk determination @@ -65,11 +61,8 @@ def determine_pk(filename, separator=","): skiprows=lambda k: k > 0 and random.random() > p, ) for i in colindex: - if ( - pd.Series(csvdata.iloc[:, i]).is_unique - and pd.Series(csvdata.iloc[:, i]).notnull().values.any() - ): + if pd.Series(csvdata.iloc[:, i]).is_unique and pd.Series(csvdata.iloc[:, i]).notnull().values.any(): j = j + 1 pk.update({list(colnames)[i]: j}) logging.info(f"Determined primary key {pk}") - return json.dumps(pk) + return pk diff --git a/dbrepo-analyse-service/determine_stats.py b/dbrepo-analyse-service/determine_stats.py index 50642ca52b..3b68aa76b6 100644 --- a/dbrepo-analyse-service/determine_stats.py +++ b/dbrepo-analyse-service/determine_stats.py @@ -1,21 +1,34 @@ +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json + from pandas import DataFrame from sqlalchemy import create_engine, text -def determine_stats(db, os, **kwargs): +@dataclass_json +@dataclass(init=True, eq=True) +class TableStats: + columns: dict[str, {"val_min": float, "val_max": float, "mean": float, "median": float, + "std_dev": float}] = field(default_factory=dict) + + +def determine_stats(db, os, **kwargs) -> TableStats: database_id = kwargs.get("database_id") table_id = kwargs.get("table_id") - with db.engine.connect() as connection: - database_name = connection.execute( - text(f"SELECT internal_name FROM mdb_databases WHERE id={database_id}") - ).fetchone()[0] - table_name = connection.execute( - text(f"SELECT internal_name FROM mdb_tables WHERE id={table_id}") - ).fetchone()[0] + try: + with db.engine.connect() as connection: + database_name = connection.execute( + text(f"SELECT internal_name FROM mdb_databases WHERE id={database_id}") + ).fetchone()[0] + table_name = connection.execute( + text(f"SELECT internal_name FROM mdb_tables WHERE id={table_id}") + ).fetchone()[0] + except Exception: + raise OSError(f"Failed to get database name and table name") if not database_name or not table_name: - return False + raise OSError(f"Failed to get database name and table name") data_db_host = kwargs.get("data_db_host", "data-db") data_db_port = kwargs.get("data_db_port", 3306) @@ -30,6 +43,7 @@ def determine_stats(db, os, **kwargs): rows = result.fetchall() df = DataFrame(rows, columns=result.keys()) + stats = TableStats() for column, dtype in df.dtypes.items(): # Check if the column has a numeric data type if dtype.kind in "fi": @@ -41,6 +55,9 @@ def determine_stats(db, os, **kwargs): "median": df[column].median(), "std_dev": df[column].std(), } + stats.columns[column] = {"val_min": float(df[column].min()), "val_max": float(df[column].max()), + "mean": float(df[column].mean()), "median": float(df[column].median()), + "std_dev": float(df[column].std())} # Store statistical properties to the metadata db and index to OS # TODO: use prepared statements to eliminate SQL injection @@ -93,4 +110,4 @@ def determine_stats(db, os, **kwargs): refresh=True, ) - return True + return stats diff --git a/dbrepo-analyse-service/test/test_determine_pk.py b/dbrepo-analyse-service/test/test_determine_pk.py index d6f1314a2b..40cf9f9b3a 100644 --- a/dbrepo-analyse-service/test/test_determine_pk.py +++ b/dbrepo-analyse-service/test/test_determine_pk.py @@ -22,8 +22,7 @@ class DeterminePrimaryKeyTest(unittest.TestCase): # test response = determine_pk('largefile_idfirst.csv') - data = json.loads(response) - self.assertEqual(1, int(data['id'])) + self.assertEqual(1, int(response['id'])) # @Test def test_determine_pk_largeFileIdInBetween_succeeds(self): @@ -33,8 +32,7 @@ class DeterminePrimaryKeyTest(unittest.TestCase): # test response = determine_pk('largefile_idinbtw.csv') - data = json.loads(response) - self.assertEqual(1, int(data['id'])) + self.assertEqual(1, int(response['id'])) # @Test def test_determine_pk_largeFileNoPrimaryKey_fails(self): @@ -44,8 +42,7 @@ class DeterminePrimaryKeyTest(unittest.TestCase): # test response = determine_pk('largefile_no_pk.csv') - data = json.loads(response) - self.assertEqual({}, data) + self.assertEqual({}, response) # @Test def test_determine_pk_largeFileNullInUnique_fails(self): @@ -55,8 +52,7 @@ class DeterminePrimaryKeyTest(unittest.TestCase): # test response = determine_pk('largefile_nullinunique.csv') - data = json.loads(response) - self.assertFalse('uniquestr' in data) + self.assertFalse('uniquestr' in response) # @Test def test_determine_pk_smallFileIdFirst_fails(self): @@ -66,8 +62,7 @@ class DeterminePrimaryKeyTest(unittest.TestCase): # test response = determine_pk('smallfile_idfirst.csv') - data = json.loads(response) - self.assertEqual(1, int(data['id'])) + self.assertEqual(1, int(response['id'])) # @Test def test_determine_pk_smallFileIdIntBetween_fails(self): @@ -77,8 +72,7 @@ class DeterminePrimaryKeyTest(unittest.TestCase): # test response = determine_pk('smallfile_idinbtw.csv') - data = json.loads(response) - self.assertEqual(1, int(data['id'])) + self.assertEqual(1, int(response['id'])) # @Test def test_determine_pk_smallFileNoPrimaryKey_fails(self): @@ -88,8 +82,7 @@ class DeterminePrimaryKeyTest(unittest.TestCase): # test response = determine_pk('smallfile_no_pk.csv') - data = json.loads(response) - self.assertEqual({}, data) + self.assertEqual({}, response) # @Test def test_determine_pk_smallFileNullInUnique_fails(self): @@ -99,8 +92,7 @@ class DeterminePrimaryKeyTest(unittest.TestCase): # test response = determine_pk('smallfile_nullinunique.csv') - data = json.loads(response) - self.assertFalse('uniquestr' in data) + self.assertFalse('uniquestr' in response) if __name__ == '__main__': diff --git a/dbrepo-gateway-service/dbrepo.conf b/dbrepo-gateway-service/dbrepo.conf index 3ae1a2b110..0410a01bb6 100644 --- a/dbrepo-gateway-service/dbrepo.conf +++ b/dbrepo-gateway-service/dbrepo.conf @@ -31,7 +31,7 @@ upstream search-db-dashboard { } upstream upload { - server upload-service:1080; + server upload-service:8080; } server { diff --git a/dbrepo-metadata-db/1_setup-schema.sql b/dbrepo-metadata-db/1_setup-schema.sql index 59c45cc5c3..9ecfb5b613 100644 --- a/dbrepo-metadata-db/1_setup-schema.sql +++ b/dbrepo-metadata-db/1_setup-schema.sql @@ -452,8 +452,8 @@ CREATE TABLE IF NOT EXISTS `mdb_related_identifiers` id bigint NOT NULL AUTO_INCREMENT, pid bigint NOT NULL, value varchar(255) NOT NULL, - type varchar(255), - relation varchar(255), + type varchar(255) NOT NULL, + relation varchar(255) NOT NULL, PRIMARY KEY (id), /* must be a single id from persistent identifier concept */ FOREIGN KEY (pid) REFERENCES mdb_identifiers (id), UNIQUE (pid, value) diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/container/ContainerCreateRequestDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/container/ContainerCreateRequestDto.java index 01aae6bbbb..a1eae5be77 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/container/ContainerCreateRequestDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/container/ContainerCreateRequestDto.java @@ -1,9 +1,13 @@ package at.tuwien.api.container; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.*; import lombok.extern.jackson.Jacksonized; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; @Getter @Setter @@ -19,6 +23,7 @@ public class ContainerCreateRequestDto { private String name; @NotBlank + @JsonProperty("image_id") @Schema(description = "Image ID") private Long imageId; @@ -30,10 +35,30 @@ public class ContainerCreateRequestDto { private Integer port; @NotBlank + @JsonProperty("sidecar_host") + @Field(name = "sidecar_host", type = FieldType.Keyword) + private String sidecarHost; + + @NotNull + @JsonProperty("sidecar_port") + @Field(name = "sidecar_port", type = FieldType.Integer) + private Integer sidecarPort; + + @JsonProperty("ui_host") + @Field(name = "ui_host", type = FieldType.Keyword) + private String uiHost; + + @JsonProperty("ui_port") + @Field(name = "ui_port", type = FieldType.Integer) + private Integer uiPort; + + @NotBlank + @JsonProperty("privileged_username") @Schema(description = "Username of privileged user", example = "root") private String privilegedUsername; @NotBlank + @JsonProperty("privileged_password") @Schema(description = "Password of privileged user") private String privilegedPassword; } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/container/ContainerDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/container/ContainerDto.java index 792d4f7756..c650edf64d 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/container/ContainerDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/container/ContainerDto.java @@ -62,6 +62,7 @@ public class ContainerDto { @Field(name = "ui_port", type = FieldType.Integer) private Integer uiPort; + @NotNull private ImageDto image; @NotNull diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseBriefDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseBriefDto.java deleted file mode 100644 index b4fd668da3..0000000000 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseBriefDto.java +++ /dev/null @@ -1,83 +0,0 @@ -package at.tuwien.api.database; - -import at.tuwien.api.container.ContainerBriefDto; -import at.tuwien.api.container.image.ImageDto; -import at.tuwien.api.database.table.TableBriefDto; -import at.tuwien.api.identifier.IdentifierDto; -import at.tuwien.api.user.UserBriefDto; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.*; -import lombok.extern.jackson.Jacksonized; - -import java.time.Instant; -import java.util.List; - -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Jacksonized -@ToString -public class DatabaseBriefDto { - - @NotNull - private Long id; - - @NotBlank - @Schema(example = "Air Quality") - private String name; - - @NotBlank - @JsonProperty("exchange_name") - @Schema(example = "dbrepo") - private String exchangeName; - - @JsonProperty("exchange_type") - @Schema(example = "topic") - private String exchangeType; - - private IdentifierDto identifier; - - @NotBlank - @JsonProperty("internal_name") - @Schema(example = "air_quality") - private String internalName; - - @Schema(example = "Air Quality") - private String description; - - @org.springframework.data.annotation.Transient - private List<TableBriefDto> tables; - - @org.springframework.data.annotation.Transient - private List<ViewBriefDto> views; - - @JsonProperty("is_public") - @Schema(example = "true") - private Boolean isPublic; - - @org.springframework.data.annotation.Transient - private ImageDto image; - - private ContainerBriefDto container; - - @org.springframework.data.annotation.Transient - private List<DatabaseAccessDto> accesses; - - @NotNull - private UserBriefDto creator; - - @NotNull - private UserBriefDto owner; - - @NotNull - @Schema(example = "2021-03-12T15:26:21Z") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") - private Instant created; - -} diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java index 9ecf26e386..fac25058b9 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java @@ -68,11 +68,13 @@ public class DatabaseDto { @Field(name = "views", type = FieldType.Object) private List<ViewDto> views; + @NotNull @JsonProperty("is_public") @Schema(example = "true") @Field(name = "is_public", type = FieldType.Boolean) private Boolean isPublic; + @NotNull @Field(name = "container", type = FieldType.Object) private ContainerDto container; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/ViewBriefDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/ViewBriefDto.java index 8cf638fb86..ffb4ccb3de 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/ViewBriefDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/ViewBriefDto.java @@ -1,6 +1,6 @@ package at.tuwien.api.database; -import at.tuwien.api.identifier.IdentifierBriefDto; +import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.api.user.UserDto; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -28,6 +28,7 @@ public class ViewBriefDto { private Long id; @NotNull + @JsonProperty("database_id") private Long vdbid; @NotBlank @@ -39,7 +40,7 @@ public class ViewBriefDto { @Schema(example = "air_quality") private String internalName; - private IdentifierBriefDto identifier; + private IdentifierDto identifier; @JsonProperty("is_public") @Schema(example = "true") diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryBriefDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryBriefDto.java index 9b48ff9ebf..64a54bcb1a 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryBriefDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryBriefDto.java @@ -1,6 +1,5 @@ package at.tuwien.api.database.query; -import at.tuwien.api.identifier.IdentifierBriefDto; import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.api.user.UserDto; import com.fasterxml.jackson.annotation.JsonFormat; @@ -75,7 +74,7 @@ public class QueryBriefDto { @Schema(example = "query") private QueryTypeDto type; - private List<IdentifierBriefDto> identifiers; + private List<IdentifierDto> identifiers; @NotNull @Schema(example = "2021-03-12T15:26:21Z") diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryDto.java index 593b65ce5b..8ba3822061 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryDto.java @@ -49,6 +49,7 @@ public class QueryDto { @Schema(example = "SELECT `id` FROM `air_quality`") private String query; + @NotBlank @JsonProperty("query_normalized") @Schema(example = "SELECT `id` FROM `air_quality`") private String queryNormalized; @@ -56,6 +57,7 @@ public class QueryDto { @Schema(example = "query") private QueryTypeDto type; + @NotNull private List<IdentifierDto> identifiers; @NotBlank(message = "query hash is required") diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryResultDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryResultDto.java index d64bfd4db3..b2b6dfe1ec 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryResultDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/query/QueryResultDto.java @@ -28,8 +28,4 @@ public class QueryResultDto { @NotNull(message = "query id is required") private Long id; - @Schema(example = "1") - @JsonProperty("result_number") - private Long resultNumber; - } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableDto.java index f4560c5a21..4975b8066a 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableDto.java @@ -36,6 +36,7 @@ public class TableDto { private Long id; @NotNull + @JsonProperty("database_id") @Field(name = "database_id", type = FieldType.Keyword) private Long tdbid; @@ -129,6 +130,7 @@ public class TableDto { @Field(name = "columns", type = FieldType.Object) private List<ColumnDto> columns; + @NotNull @Field(name = "constraints", type = FieldType.Object) private ConstraintsDto constraints; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/ConstraintsCreateDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/ConstraintsCreateDto.java index 6fc2304f5d..033ec75a81 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/ConstraintsCreateDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/ConstraintsCreateDto.java @@ -6,6 +6,7 @@ import lombok.*; import lombok.extern.jackson.Jacksonized; import java.util.List; +import java.util.Set; @Getter @Setter @@ -21,6 +22,6 @@ public class ConstraintsCreateDto { @JsonProperty("foreign_keys") private List<ForeignKeyCreateDto> foreignKeys = null; - private List<String> checks = null; + private Set<String> checks = null; } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/ConstraintsDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/ConstraintsDto.java index 6b5e0e7d76..4696e7d1a4 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/ConstraintsDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/ConstraintsDto.java @@ -9,6 +9,7 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import java.util.List; +import java.util.Set; @Getter @Setter @@ -27,5 +28,5 @@ public class ConstraintsDto { private List<ForeignKeyDto> foreignKeys; @org.springframework.data.annotation.Transient - private List<String> checks; + private Set<String> checks; } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreignKey/ForeignKeyDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreignKey/ForeignKeyDto.java index e577b033c4..f3416dc108 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreignKey/ForeignKeyDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreignKey/ForeignKeyDto.java @@ -19,15 +19,19 @@ import java.util.List; @ToString public class ForeignKeyDto { + @NonNull private String name; + @NonNull @org.springframework.data.annotation.Transient private List<ColumnDto> columns; + @NonNull @JsonProperty("referenced_table") @org.springframework.data.annotation.Transient private TableBriefDto referencedTable; + @NonNull @JsonProperty("referenced_columns") @org.springframework.data.annotation.Transient private List<ColumnDto> referencedColumns; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/CreatorSaveDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/CreatorSaveDto.java index 3e837bc32d..2c05d1d6f1 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/CreatorSaveDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/CreatorSaveDto.java @@ -16,11 +16,9 @@ import lombok.extern.jackson.Jacksonized; @ToString public class CreatorSaveDto { - @NotBlank @Schema(example = "Josiah") private String firstname; - @NotBlank @Schema(example = "Carberry") private String lastname; @@ -29,7 +27,6 @@ public class CreatorSaveDto { @Schema(example = "Carberry, Josiah") private String creatorName; - @NotBlank @JsonProperty("name_type") @Schema(example = "Personal") private NameTypeDto nameType; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierBriefDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierBriefDto.java deleted file mode 100644 index 893a007e13..0000000000 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierBriefDto.java +++ /dev/null @@ -1,72 +0,0 @@ -package at.tuwien.api.identifier; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.*; -import lombok.extern.jackson.Jacksonized; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -import java.time.Instant; -import java.util.List; - -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Jacksonized -@ToString -public class IdentifierBriefDto { - - @NotNull - private Long id; - - @JsonProperty("database_id") - @Field(name = "database_id") - @Schema(example = "1") - private Long databaseId; - - @JsonProperty("query_id") - @Field(name = "query_id") - @Schema(example = "1") - private Long queryId; - - @NotNull - private IdentifierTypeDto type; - - private List<IdentifierTitleDto> titles; - - @Schema(example = "10.1038/nphys1170") - private String doi; - - @Schema(example = "TU Wien") - private String publisher; - - @NotNull - @JsonProperty("publication_year") - @Field(name = "publication_year") - @Schema(example = "2022") - private Integer publicationYear; - - @NotNull - private List<CreatorBriefDto> creators; - - @JsonIgnore - @Field(type = FieldType.Date) - @Schema(example = "2021-03-12T15:26:21Z") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") - private Instant created; - - @JsonIgnore - @org.springframework.data.annotation.Transient - @Field(type = FieldType.Date) - @JsonProperty("last_modified") - @Schema(example = "2021-03-12T15:26:21Z") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") - private Instant lastModified; - -} diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierDto.java index a405eece1b..fdb4a3e62d 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierDto.java @@ -34,6 +34,7 @@ public class IdentifierDto { @Field(name = "id", type = FieldType.Keyword) private Long id; + @NotNull @JsonProperty("database_id") @Schema(example = "1") @Field(name = "database_id", type = FieldType.Keyword) @@ -58,6 +59,7 @@ public class IdentifierDto { @Field(name = "type", type = FieldType.Keyword) private IdentifierTypeDto type; + @NotNull @Field(name = "titles", type = FieldType.Object) private List<IdentifierTitleDto> titles; @@ -88,19 +90,16 @@ public class IdentifierDto { @Field(name = "query_hash", type = FieldType.Text) private String queryHash; - @NotNull @Field(name = "execution", type = FieldType.Date) @Schema(example = "2021-03-12T15:26:21Z") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") private Instant execution; - @NotBlank @JsonProperty("result_hash") @Field(name = "result_hash", type = FieldType.Text) @Schema(example = "34fe82cda2c53f13f8d90cfd7a3469e3a939ff311add50dce30d9136397bf8e5") private String resultHash; - @NotNull @JsonProperty("result_number") @Field(name = "result_number", type = FieldType.Long) @Schema(example = "1") @@ -110,6 +109,7 @@ public class IdentifierDto { @Field(name = "doi", type = FieldType.Keyword) private String doi; + @NotBlank @Schema(example = "TU Wien") @Field(name = "publisher", type = FieldType.Text) private String publisher; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveDescriptionDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveDescriptionDto.java index b0eac7d071..1c8ab5146d 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveDescriptionDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveDescriptionDto.java @@ -3,6 +3,7 @@ package at.tuwien.api.identifier; import at.tuwien.api.database.LanguageTypeDto; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import lombok.*; import lombok.extern.jackson.Jacksonized; @@ -15,6 +16,7 @@ import lombok.extern.jackson.Jacksonized; @ToString public class IdentifierSaveDescriptionDto { + @NotBlank @Schema(example = "Air quality reports at Stephansplatz, Vienna") private String description; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveDto.java index 084b783eb9..e88cef16c1 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveDto.java @@ -4,6 +4,7 @@ import at.tuwien.api.database.LanguageTypeDto; import at.tuwien.api.database.LicenseDto; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.*; @@ -41,6 +42,7 @@ public class IdentifierSaveDto { @Schema(example = "database") private IdentifierTypeDto type; + @NotNull private List<IdentifierSaveTitleDto> titles; private List<IdentifierSaveDescriptionDto> descriptions; @@ -57,6 +59,7 @@ public class IdentifierSaveDto { @Schema(example = "12") private Integer publicationMonth; + @NotBlank @Schema(example = "TU Wien") private String publisher; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveTitleDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveTitleDto.java index c850f8f57e..039d856b60 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveTitleDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/IdentifierSaveTitleDto.java @@ -3,6 +3,7 @@ package at.tuwien.api.identifier; import at.tuwien.api.database.LanguageTypeDto; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import lombok.*; import lombok.extern.jackson.Jacksonized; @@ -15,6 +16,7 @@ import lombok.extern.jackson.Jacksonized; @ToString public class IdentifierSaveTitleDto { + @NotBlank @Schema(example = "Airquality Demonstrator") private String title; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/RelatedIdentifierDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/RelatedIdentifierDto.java index 962a743d98..1398710b7b 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/RelatedIdentifierDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/RelatedIdentifierDto.java @@ -34,10 +34,12 @@ public class RelatedIdentifierDto { @Field(name = "value", type = FieldType.Keyword) private String value; + @NotNull @Schema(example = "DOI") @Field(name = "type", type = FieldType.Keyword) private RelatedTypeDto type; + @NotNull @Schema(example = "Cites") @Field(name = "relation", type = FieldType.Keyword) private RelationTypeDto relation; @@ -48,18 +50,6 @@ public class RelatedIdentifierDto { @org.springframework.data.annotation.Transient private UserDto creator; - @NotNull - @Field(name = "created", type = FieldType.Date) - @Schema(example = "2021-03-12T15:26:21Z") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") - private Instant created; - - @JsonProperty("last_modified") - @Schema(example = "2021-03-12T15:26:21Z") - @org.springframework.data.annotation.Transient - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") - private Instant lastModified; - } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/RelatedIdentifierSaveDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/RelatedIdentifierSaveDto.java index 2d7a2992af..89512e42c3 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/RelatedIdentifierSaveDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/identifier/RelatedIdentifierSaveDto.java @@ -19,9 +19,11 @@ public class RelatedIdentifierSaveDto { @Schema(example = "10.70124/dc4zh-9ce78") private String value; + @NotNull @Schema(example = "DOI") private RelatedTypeDto type; + @NotNull @Schema(example = "Cites") private RelationTypeDto relation; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserAttributesDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserAttributesDto.java index bbe3b17f73..617fc7c260 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserAttributesDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserAttributesDto.java @@ -3,6 +3,7 @@ package at.tuwien.api.user; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.*; import lombok.extern.jackson.Jacksonized; import org.springframework.data.elasticsearch.annotations.Field; @@ -17,6 +18,7 @@ import org.springframework.data.elasticsearch.annotations.FieldType; @ToString public class UserAttributesDto { + @NotNull @org.springframework.data.annotation.Transient @Schema(example = "light") private String theme; diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/FormatNotAvailableException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/FormatNotAvailableException.java new file mode 100644 index 0000000000..4ca41e346d --- /dev/null +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/FormatNotAvailableException.java @@ -0,0 +1,23 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.io.IOException; + +@ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE) +public class FormatNotAvailableException extends IOException { + + public FormatNotAvailableException(String msg) { + super(msg); + } + + public FormatNotAvailableException(String msg, Throwable thr) { + super(msg + ": " + thr.getLocalizedMessage(), thr); + } + + public FormatNotAvailableException(Throwable thr) { + super(thr); + } + +} diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/DatabaseMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/DatabaseMapper.java index 764d79cc29..8f4d2b07b3 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/DatabaseMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/DatabaseMapper.java @@ -6,7 +6,6 @@ import at.tuwien.entities.container.image.ContainerImage; import at.tuwien.entities.database.*; import at.tuwien.entities.user.User; import at.tuwien.exception.QueryMalformedException; -import org.apache.commons.lang3.RandomStringUtils; import org.apache.http.auth.BasicUserPrincipal; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -21,7 +20,7 @@ import java.text.Normalizer; import java.util.Locale; import java.util.regex.Pattern; -@Mapper(componentModel = "spring", uses = {ContainerMapper.class, UserMapper.class, ImageMapper.class, UserMapper.class, TableMapper.class, IdentifierMapper.class, ViewMapper.class}, imports = {RandomStringUtils.class}) +@Mapper(componentModel = "spring", uses = {ContainerMapper.class, UserMapper.class, ImageMapper.class, UserMapper.class, TableMapper.class, IdentifierMapper.class, ViewMapper.class}) public interface DatabaseMapper { org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DatabaseMapper.class); @@ -51,25 +50,12 @@ public interface DatabaseMapper { return data.getName() + ":" + data.getVersion(); } - @Mappings({ - @Mapping(target = "id", source = "id"), - @Mapping(target = "image", source = "container.image"), - @Mapping(target = "created", source = "created", dateFormat = "dd-MM-yyyy HH:mm"), - @Mapping(target = "container", ignore = true), - }) - DatabaseBriefDto databaseToDatabaseBriefDto(Database data); - @Mappings({ @Mapping(target = "id", source = "id"), @Mapping(target = "created", source = "created", dateFormat = "dd-MM-yyyy HH:mm"), }) DatabaseDto databaseToDatabaseDto(Database data); - @Mappings({ - @Mapping(target = "internalName", expression = "java(nameToInternalName(data.getName()) + \"_\" + RandomStringUtils.randomAlphabetic(4).toLowerCase())"), - }) - Database databaseCreateDtoToDatabase(DatabaseCreateDto data); - AccessType accessTypeDtoToAccessType(AccessTypeDto data); AccessTypeDto accessTypeToAccessTypeDto(AccessType data); diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/IdentifierMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/IdentifierMapper.java index 985dcc7271..0c2a048d5c 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/IdentifierMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/IdentifierMapper.java @@ -4,14 +4,12 @@ import at.tuwien.api.identifier.*; import at.tuwien.api.identifier.ld.LdCreatorDto; import at.tuwien.api.identifier.ld.LdDatasetDto; import at.tuwien.entities.identifier.*; -import lombok.extern.log4j.Log4j2; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.Named; import java.util.List; -import java.util.Objects; import java.util.Optional; @Mapper(componentModel = "spring", uses = {DatabaseMapper.class}) @@ -21,8 +19,6 @@ public interface IdentifierMapper { Identifier identifierDtoToIdentifier(IdentifierDto data); - IdentifierBriefDto identifierToIdentifierBriefDto(Identifier data); - @Mappings({ @Mapping(target = "database.identifiers", ignore = true), }) diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/QueryMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/QueryMapper.java index fa9bbd19df..355e1d1487 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/QueryMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/QueryMapper.java @@ -193,14 +193,18 @@ public interface QueryMapper { .append("`") .append(column.getInternalName()) .append("` = "); - log.trace("import has null element present"); - set.append("IF(!STRCMP(@") - .append(column.getInternalName()) - .append(",'") - .append(data.getNullElement()) - .append("'),NULL,"); + if (data.getNullElement() != null) { + log.trace("import has null element present"); + set.append("IF(!STRCMP(@") + .append(column.getInternalName()) + .append(",'") + .append(data.getNullElement()) + .append("'),NULL,"); + columnToBoolSet2(data, column, set); + set.append(")"); + return; + } columnToBoolSet2(data, column, set); - set.append(")"); } default void columnToBoolSet2(ImportDto data, TableColumn column, StringBuilder set) { @@ -265,14 +269,19 @@ public interface QueryMapper { .append("`") .append(column.getInternalName()) .append("` = "); - log.trace("import has null element present"); - set.append("IF(STRCMP(@") - .append(column.getInternalName()) - .append(",'") - .append(data.getNullElement()) - .append("'), @") - .append(column.getInternalName()) - .append(", NULL)"); + if (data.getNullElement() != null) { + log.trace("import has null element present"); + set.append("IF(STRCMP(@") + .append(column.getInternalName()) + .append(",'") + .append(data.getNullElement()) + .append("'), @") + .append(column.getInternalName()) + .append(", NULL)"); + return; + } + set.append("@") + .append(column.getInternalName()); } default void columnToDateSet(ImportDto data, TableColumn column, StringBuilder set) { @@ -281,14 +290,24 @@ public interface QueryMapper { .append("`") .append(column.getInternalName()) .append("` = STR_TO_DATE("); - log.trace("import has null element present"); - set.append("IF(STRCMP(@") - .append(column.getInternalName()) - .append(",'") - .append(data.getNullElement()) - .append("'), @") + if (data.getNullElement() != null) { + log.trace("import has null element present"); + set.append("IF(STRCMP(@") + .append(column.getInternalName()) + .append(",'") + .append(data.getNullElement()) + .append("'), @") + .append(column.getInternalName()) + .append(", NULL), '") + .append(column.getDateFormat() + .getDatabaseFormat() + .replace('\'', '\\')) + .append("')"); + return; + } + set.append("@") .append(column.getInternalName()) - .append(", NULL), '") + .append(", '") .append(column.getDateFormat() .getDatabaseFormat() .replace('\'', '\\')) diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/TableMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/TableMapper.java index b3b734d75e..0626da4d08 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/TableMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/TableMapper.java @@ -61,7 +61,7 @@ public interface TableMapper { @Mapping(target = "internalName", expression = "java(data.getInternalName())"), @Mapping(target = "queueName", expression = "java(data.getQueueName())"), @Mapping(target = "routingKey", expression = "java(data.getRoutingKey())"), - @Mapping(source = "description", target = "description") + @Mapping(target = "isPublic", source = "database.isPublic") }) TableDto tableToTableDto(Table data); @@ -204,6 +204,9 @@ public interface TableMapper { return null; } final Constraints.ConstraintsBuilder builder = Constraints.builder(); + if (data.getChecks() != null) { + builder.checks(data.getChecks()); + } if (data.getUniques() != null) { final List<Unique> uniques = new ArrayList<>(); for (List<String> columns : data.getUniques()) { diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index 4a95b9e4f3..18e19b5328 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -76,7 +76,7 @@ public class DatabaseEndpoint { this.messageQueueService = messageQueueService; } - @GetMapping + @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) @Observed(name = "dbr_database_findall") @Operation(summary = "List databases") @@ -85,7 +85,7 @@ public class DatabaseEndpoint { description = "List of databases", content = {@Content( mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = DatabaseBriefDto.class)))}), + array = @ArraySchema(schema = @Schema(implementation = DatabaseDto.class)))}), @ApiResponse(responseCode = "404", description = "User not found", content = {@Content( @@ -110,45 +110,11 @@ public class DatabaseEndpoint { .collect(Collectors.toList()); } log.trace("list databases resulted in databases {}", dtos); - return ResponseEntity.ok(dtos); - } - - @RequestMapping(method = RequestMethod.HEAD) - @Transactional(readOnly = true) - @Observed(name = "dbr_database_count") - @Operation(summary = "Count databases") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", - description = "Count databases"), - @ApiResponse(responseCode = "404", - description = "User not found", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - }) - public ResponseEntity<List<DatabaseDto>> count(@NotNull Principal principal, - @RequestParam(required = false) String filter) - throws UserNotFoundException { - log.debug("endpoint list databases, filter={}, {}", filter, PrincipalUtil.formatForDebug(principal)); - final List<DatabaseDto> dtos; - if (principal != null && filter != null) { - final User user = userService.findByUsername(principal.getName()); - dtos = databaseService.findAccess(user.getId()) - .stream() - .map(databaseMapper::databaseToDatabaseDto) - .collect(Collectors.toList()); - } else { - dtos = databaseService.findAll() - .stream() - .map(databaseMapper::databaseToDatabaseDto) - .collect(Collectors.toList()); - } - log.trace("list databases resulted in databases {}", dtos); final HttpHeaders headers = new HttpHeaders(); - headers.set("x-count", "" + dtos.size()); + headers.set("X-Count", "" + dtos.size()); return ResponseEntity.status(HttpStatus.OK) .headers(headers) - .build(); + .body(dtos); } @PostMapping @@ -161,7 +127,7 @@ public class DatabaseEndpoint { description = "Created a new database", content = {@Content( mediaType = "application/json", - schema = @Schema(implementation = DatabaseBriefDto.class))}), + schema = @Schema(implementation = DatabaseDto.class))}), @ApiResponse(responseCode = "400", description = "Database create query is malformed or image is not supported", content = {@Content( @@ -188,8 +154,8 @@ public class DatabaseEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<DatabaseBriefDto> create(@Valid @RequestBody DatabaseCreateDto createDto, - @NotNull Principal principal) + public ResponseEntity<DatabaseDto> create(@Valid @RequestBody DatabaseCreateDto createDto, + @NotNull Principal principal) throws ContainerNotFoundException, DatabaseMalformedException, UserNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, QueryMalformedException, NotAllowedException, QueryStoreException { @@ -200,7 +166,7 @@ public class DatabaseEndpoint { accessService.create(database.getId(), user.getId(), DatabaseGiveAccessDto.builder() .type(AccessTypeDto.WRITE_ALL) .build()); - final DatabaseBriefDto dto = databaseMapper.databaseToDatabaseBriefDto(database); + final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(database); log.trace("create database resulted in database {}", dto); return ResponseEntity.status(HttpStatus.CREATED) .body(dto); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java index a45c51d5fd..fdb0391406 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java @@ -84,11 +84,6 @@ public class IdentifierEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = IdentifierDto.class))}), - @ApiResponse(responseCode = "204", - description = "Identifier could not be created", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "400", description = "Identifier form contains invalid request data", content = {@Content( @@ -109,21 +104,6 @@ public class IdentifierEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "406", - description = "Creating identifier not allowed", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "409", - description = "Identifier for this resource already exists", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "502", - description = "Query information could not be retrieved", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "503", description = "DataCite system did not respond", content = {@Content( @@ -134,7 +114,7 @@ public class IdentifierEndpoint { @NotNull Principal principal) throws DatabaseNotFoundException, NotAllowedException, IdentifierRequestException, ViewNotFoundException, TableNotFoundException, QueryStoreException, QueryNotFoundException, ImageNotSupportedException, UserNotFoundException, - DatabaseConnectionException, RemoteUnavailableException { + DatabaseConnectionException { log.debug("endpoint create identifier, data={}, {}", data, PrincipalUtil.formatForDebug(principal)); /* check data */ if (!endpointValidator.validatePublicationDate(data)) { @@ -213,9 +193,14 @@ public class IdentifierEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = IdentifierDto.class))}), + @ApiResponse(responseCode = "404", + description = "Failed to find metadata for identifier", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<ExternalMetadataDto> retrieve(@NotNull @Valid @RequestParam String url) - throws OrcidNotFoundException, RorNotFoundException, RemoteUnavailableException, DoiNotFoundException { + throws OrcidNotFoundException, RorNotFoundException, DoiNotFoundException, IdentifierNotFoundException { return ResponseEntity.ok(metadataService.findByUrl(url)); } diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java index 94ca20ea03..10b349db49 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java @@ -55,7 +55,7 @@ public class PersistenceEndpoint { this.identifierService = identifierService; } - @GetMapping + @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, "application/ld+json"}) @Transactional(readOnly = true) @Observed(name = "dbr_pid_findall") @Operation(summary = "Find all identifiers") @@ -66,22 +66,17 @@ public class PersistenceEndpoint { @Content(mediaType = "application/json", schema = @Schema(implementation = IdentifierDto[].class)), @Content(mediaType = "application/ld+json", schema = @Schema(implementation = LdDatasetDto[].class)) }), - @ApiResponse(responseCode = "400", + @ApiResponse(responseCode = "406", description = "Identifier could not be exported, the requested style is not known", content = {@Content( - mediaType = "text/bibliography", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "404", - description = "Identifier could not be found", - content = {@Content( - mediaType = "text/csv", + mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<?> findAll(@Valid @RequestParam(value = "dbid", required = false) Long dbid, @Valid @RequestParam(value = "qid", required = false) Long qid, @Valid @RequestParam(value = "vid", required = false) Long vid, @Valid @RequestParam(value = "tid", required = false) Long tid, - @RequestHeader(HttpHeaders.ACCEPT) String accept) throws NotAllowedException { + @RequestHeader(HttpHeaders.ACCEPT) String accept) throws FormatNotAvailableException { log.debug("endpoint find identifiers, dbid={}, qid={}, vid={}, tid={}, accept={}", dbid, qid, vid, tid, accept); final List<Identifier> identifiers = identifierService.findAll() .stream() @@ -110,11 +105,13 @@ public class PersistenceEndpoint { log.debug("find identifier resulted in identifiers {}", resource2); return ResponseEntity.ok(resource2); } - throw new NotAllowedException("Must provide either application/json or application/ld+json headers"); + throw new FormatNotAvailableException("Must provide either application/json or application/ld+json headers"); } - @GetMapping(value = "/{pid}", produces = MediaType.ALL_VALUE) + @GetMapping(value = "/{pid}", produces = {MediaType.APPLICATION_JSON_VALUE, "application/ld+json", + MediaType.TEXT_XML_VALUE, "text/csv", "text/bibliography", "text/bibliography; style=apa", + "text/bibliography; style=ieee", "text/bibliography; style=bibtex"}) @Transactional(readOnly = true) @Observed(name = "dbr_pid_find") @Operation(summary = "Find some identifier") diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/QueryEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/QueryEndpoint.java index 587ceef99d..3f274a8924 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/QueryEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/QueryEndpoint.java @@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.extern.log4j.Log4j2; @@ -121,12 +122,12 @@ public class QueryEndpoint { .body(result); } - @GetMapping("/{queryId}/data") + @RequestMapping(value = "/{queryId}/data", method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) @Observed(name = "dbr_query_reexecute") @Operation(summary = "Re-execute some query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { - @ApiResponse(responseCode = "202", + @ApiResponse(responseCode = "200", description = "Executed query", content = {@Content( mediaType = "application/json", @@ -160,6 +161,7 @@ public class QueryEndpoint { public ResponseEntity<QueryResultDto> reExecute(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("queryId") Long queryId, Principal principal, + @NotNull HttpServletRequest request, @RequestParam(value = "page", required = false) Long page, @RequestParam(value = "size", required = false) Long size, @RequestParam(required = false) SortType sortDirection, @@ -173,59 +175,21 @@ public class QueryEndpoint { endpointValidator.validateOnlyAccessOrPublic(databaseId, principal); /* execute */ final Query query = storeService.findOne(databaseId, queryId, principal); - final QueryResultDto result = queryService.reExecute(databaseId, query, page, size, sortDirection, sortColumn, - principal); - result.setId(queryId); - log.trace("re-execute query resulted in result {}", result); - return ResponseEntity.status(HttpStatus.ACCEPTED) - .body(result); - } - - @GetMapping("/{queryId}/data/count") - @Transactional(readOnly = true) - @Observed(name = "dbr_query_reexecute_count") - @Operation(summary = "Re-execute some query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) - @ApiResponses(value = { - @ApiResponse(responseCode = "202", - description = "Executed query", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = QueryResultDto.class))}), - @ApiResponse(responseCode = "400", - description = "Image is not supported", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "403", - description = "Execute query not permitted", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "404", - description = "Database or query could not be found", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "417", - description = "Could not parse columns", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}) - }) - public ResponseEntity<Long> reExecuteCount(@NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("queryId") Long queryId, - Principal principal) - throws QueryStoreException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, - QueryMalformedException, TableMalformedException, ColumnParseException, NotAllowedException, - AccessDeniedException { - log.debug("endpoint re-execute query count, databaseId={}, queryId={}, {}", databaseId, queryId, PrincipalUtil.formatForDebug(principal)); - endpointValidator.validateOnlyAccessOrPublic(databaseId, principal); - /* execute */ - final Query query = storeService.findOne(databaseId, queryId, principal); - final Long result = queryService.reExecuteCount(databaseId, query, principal); - log.trace("re-execute query count resulted in result {}", result); - return ResponseEntity.status(HttpStatus.ACCEPTED) - .body(result); + final Long count = queryService.reExecuteCount(databaseId, query, principal); + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Count", "" + count); + if (request.getMethod().equals("GET")) { + final QueryResultDto result = queryService.reExecute(databaseId, query, page, size, sortDirection, sortColumn, + principal); + result.setId(queryId); + log.trace("re-execute query resulted in result {}", result); + return ResponseEntity.ok() + .headers(headers) + .body(result); + } + return ResponseEntity.ok() + .headers(headers) + .build(); } @GetMapping(value = "/{queryId}/export", produces = MediaType.ALL_VALUE) diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/StoreEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/StoreEndpoint.java index 2c520a6479..c5751ed588 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/StoreEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/StoreEndpoint.java @@ -4,7 +4,7 @@ import at.tuwien.api.database.query.QueryBriefDto; import at.tuwien.api.database.query.QueryDto; import at.tuwien.api.database.query.QueryPersistDto; import at.tuwien.api.error.ApiErrorDto; -import at.tuwien.api.identifier.IdentifierBriefDto; +import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.api.user.UserDto; import at.tuwien.entities.identifier.Identifier; import at.tuwien.exception.*; @@ -42,8 +42,6 @@ import java.security.Principal; import java.util.List; import java.util.stream.Collectors; -import static org.apache.jena.sparql.vocabulary.VocabTestQuery.query; - @Log4j2 @RestController @RequestMapping(path = "/api/database/{databaseId}/query", @@ -84,6 +82,11 @@ public class StoreEndpoint { content = {@Content( mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = QueryBriefDto.class)))}), + @ApiResponse(responseCode = "403", + description = "Find all queries is not permitted", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "404", description = "Database, container or user could not be found", content = {@Content( @@ -126,9 +129,9 @@ public class StoreEndpoint { /* find all from data database */ final List<Query> queries = storeService.findAll(databaseId, persisted, principal); /* add identifiers and creator from metadata database */ - final List<IdentifierBriefDto> identifiers = identifierService.findAllSubsetIdentifiers() + final List<IdentifierDto> identifiers = identifierService.findAllSubsetIdentifiers() .stream() - .map(identifierMapper::identifierToIdentifierBriefDto) + .map(identifierMapper::identifierToIdentifierDto) .toList(); final List<UserDto> users = userService.findAll() .stream() @@ -161,6 +164,11 @@ public class StoreEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = QueryDto.class))}), + @ApiResponse(responseCode = "403", + description = "Find query is not permitted", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "404", description = "Database, query or user could not be found", content = {@Content( @@ -217,7 +225,7 @@ public class StoreEndpoint { @Observed(name = "dbr_query_persist") @Operation(summary = "Persist some query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { - @ApiResponse(responseCode = "200", + @ApiResponse(responseCode = "202", description = "Persist query successful", content = {@Content( mediaType = "application/json", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableDataEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableDataEndpoint.java index f017e7f9e4..8c3169f700 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableDataEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableDataEndpoint.java @@ -21,10 +21,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -84,7 +87,7 @@ public class TableDataEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> insert(@NotNull @PathVariable("databaseId") Long databaseId, + public ResponseEntity<Void> insert(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("tableId") Long tableId, @NotNull @Valid @RequestBody TableCsvDto data, @NotNull Principal principal) @@ -246,7 +249,7 @@ public class TableDataEndpoint { @Observed(name = "dbr_table_data_findall") @Operation(summary = "Find data", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { - @ApiResponse(responseCode = "202", + @ApiResponse(responseCode = "200", description = "Get table data successfully"), @ApiResponse(responseCode = "400", description = "Table data is malformed or image is not supported", @@ -263,8 +266,8 @@ public class TableDataEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "422", - description = "Could not import csv via sidecar", + @ApiResponse(responseCode = "409", + description = "Result number could not be retrieved from the query store", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @@ -272,6 +275,7 @@ public class TableDataEndpoint { public ResponseEntity<QueryResultDto> getAll(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("tableId") Long tableId, @NotNull Principal principal, + @NotNull HttpServletRequest request, @RequestParam(required = false) Instant timestamp, @RequestParam(required = false) Long page, @RequestParam(required = false) Long size, @@ -279,7 +283,7 @@ public class TableDataEndpoint { @RequestParam(required = false) String sortColumn) throws TableNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, TableMalformedException, PaginationException, QueryMalformedException, SortException, NotAllowedException, - AccessDeniedException { + AccessDeniedException, QueryStoreException { log.debug("endpoint find table data, databaseId={}, tableId={}, timestamp={}, page={}, size={}, sortDirection={}, sortColumn={}, {}", databaseId, tableId, timestamp, page, size, sortDirection, sortColumn, PrincipalUtil.formatForDebug(principal)); /* check */ @@ -300,60 +304,19 @@ public class TableDataEndpoint { size = 10L; } /* find */ - final QueryResultDto response = queryService.tableFindAll(databaseId, tableId, timestamp, page, size, principal); - log.trace("find table data resulted in result {}", response); - return ResponseEntity.ok() - .body(response); - } - - @GetMapping("/count") - @Transactional(readOnly = true) - @Observed(name = "dbr_table_data_countall") - @Operation(summary = "Find data", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) - @ApiResponses(value = { - @ApiResponse(responseCode = "202", - description = "Get table data count successfully"), - @ApiResponse(responseCode = "400", - description = "Table data is malformed or image is not supported", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "403", - description = "Access to the database is forbidden", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "404", - description = "Table or database could not be found", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "422", - description = "Could not import csv via sidecar", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - }) - public ResponseEntity<Long> getCount(@NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("tableId") Long tableId, - @NotNull Principal principal, - @RequestParam(required = false) Instant timestamp) - throws TableNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, - TableMalformedException, QueryStoreException, QueryMalformedException, NotAllowedException, - AccessDeniedException { - log.debug("endpoint find table data, databaseId={}, tableId={}, timestamp={}, {}", databaseId, tableId, timestamp, PrincipalUtil.formatForDebug(principal)); - /* check */ - endpointValidator.validateOnlyAccessOrPublic(databaseId, principal); - final Database database = databaseService.find(databaseId); - if (!database.getIsPublic() && !UserUtil.hasRole(principal, "view-table-data")) { - log.error("Failed to view table data: database with id {} is private and user has no authority", databaseId); - throw new NotAllowedException("Failed to view table data: database with id " + databaseId + " is private and user has no authority"); - } - /* find */ final Long count = queryService.tableCount(databaseId, tableId, timestamp, principal); - log.debug("find table data count resulted in {} tuple(s)", count); + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Count", "" + count); + if (request.getMethod().equals("GET")) { + final QueryResultDto response = queryService.tableFindAll(databaseId, tableId, timestamp, page, size, principal); + log.trace("find table data resulted in result {}", response); + return ResponseEntity.ok() + .headers(headers) + .body(response); + } return ResponseEntity.ok() - .body(count); + .headers(headers) + .build(); } } diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index e658072a6a..ec1f5f655c 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -128,12 +128,12 @@ public class TableEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<TableBriefDto> create(@NotNull @PathVariable("databaseId") Long databaseId, + public ResponseEntity<TableDto> create(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @Valid @RequestBody TableCreateDto createDto, @NotNull Principal principal) throws ImageNotSupportedException, DatabaseNotFoundException, TableMalformedException, TableNameExistsException, QueryMalformedException, NotAllowedException, AccessDeniedException, - TableNotFoundException { + TableNotFoundException, UserNotFoundException { log.debug("endpoint create table, databaseId={}, createDto={}, {}", databaseId, createDto, PrincipalUtil.formatForDebug(principal)); /* checks */ if (createDto.getName().isBlank()) { @@ -143,7 +143,7 @@ public class TableEndpoint { endpointValidator.validateOnlyAccess(databaseId, principal, true); endpointValidator.validateColumnCreateConstraints(createDto); final Table table = tableService.createTable(databaseId, createDto, principal); - final TableBriefDto dto = tableMapper.tableToTableBriefDto(table); + final TableDto dto = tableMapper.tableToTableDto(table); log.trace("create table resulted in table {}", dto); return ResponseEntity.status(HttpStatus.CREATED) .body(dto); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java index 996f2c5d59..14fee21e4d 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java @@ -23,10 +23,12 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -199,7 +201,7 @@ public class ViewEndpoint { @Observed(name = "dbr_view_delete") @Operation(summary = "Delete one view", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { - @ApiResponse(responseCode = "200", + @ApiResponse(responseCode = "202", description = "Delete view successfully", content = {@Content}), @ApiResponse(responseCode = "400", @@ -250,7 +252,7 @@ public class ViewEndpoint { .build(); } - @GetMapping("/{viewId}/data") + @RequestMapping(value = "/{viewId}/data", method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) @Observed(name = "dbr_view_data_findall") @Operation(summary = "Find view data", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @@ -279,10 +281,12 @@ public class ViewEndpoint { public ResponseEntity<QueryResultDto> data(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("viewId") Long viewId, Principal principal, + @NotNull HttpServletRequest request, @RequestParam(required = false) Long page, @RequestParam(required = false) Long size) throws DatabaseNotFoundException, NotAllowedException, ViewNotFoundException, PaginationException, - TableMalformedException, QueryMalformedException, UserNotFoundException { + TableMalformedException, QueryMalformedException, UserNotFoundException, QueryStoreException, + ImageNotSupportedException { log.debug("endpoint find view data, databaseId={}, viewId={}, page={}, size={}, {}", databaseId, viewId, page, size, PrincipalUtil.formatForDebug(principal)); /* check */ endpointValidator.validateDataParams(page, size); @@ -309,59 +313,20 @@ public class ViewEndpoint { /* find */ log.debug("find view data for database with id {}", databaseId); final View view = viewService.findById(databaseId, viewId, principal); - final QueryResultDto result = queryService.viewFindAll(databaseId, view, page, size, principal); - log.trace("execute view data for view with id {}", viewId); - log.debug("find view data resulted in result {}", result); - return ResponseEntity.ok() - .body(result); - } - - @GetMapping("/{viewId}/data/count") - @Transactional(readOnly = true) - @Observed(name = "dbr_view_data_count") - @Operation(summary = "Find view data count", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", - description = "Count data successfully", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))}), - @ApiResponse(responseCode = "400", - description = "Pagination not in valid range or find data query is malformed", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "403", - description = "Count data not allowed", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "404", - description = "Database, view, container or user could not be found", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "409", - description = "Could not count query data", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}) - }) - public ResponseEntity<Long> count(@NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("viewId") Long viewId, - Principal principal) - throws DatabaseNotFoundException, ViewNotFoundException, QueryStoreException, TableMalformedException, - QueryMalformedException, ImageNotSupportedException, UserNotFoundException { - log.debug("endpoint find view data count, databaseId={}, viewId={}, {}", databaseId, viewId, PrincipalUtil.formatForDebug(principal)); - /* find */ - databaseService.find(databaseId); - log.debug("find view data count for database with id {}", databaseId); - final View view = viewService.findById(databaseId, viewId, principal); - final Long result = queryService.viewCount(databaseId, view, principal); - log.trace("execute view data count for view with id {}", viewId); - log.debug("find view data count resulted in result {}", result); + final Long count = queryService.viewCount(databaseId, view, principal); + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Count", "" + count); + if (request.getMethod().equals("GET")) { + final QueryResultDto result = queryService.viewFindAll(databaseId, view, page, size, principal); + log.trace("execute view data for view with id {}", viewId); + log.debug("find view data resulted in result {}", result); + return ResponseEntity.ok() + .headers(headers) + .body(result); + } return ResponseEntity.ok() - .body(result); + .headers(headers) + .build(); } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java index d3c6fb1813..bc4632caee 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java @@ -185,47 +185,6 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { list_generic(DATABASE_3_ID, CONTAINER_3, List.of(DATABASE_3), USER_1_PRINCIPAL, null); } - @Test - @WithAnonymousUser - public void count_anonymous_succeeds() throws UserNotFoundException { - - /* pre-condition */ - assertFalse(DATABASE_1_PUBLIC); - - /* test */ - count_generic(DATABASE_1_ID, List.of(DATABASE_1), null, null, null); - } - - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"list-databases"}) - public void count_hasRole_succeeds() throws UserNotFoundException { - - /* pre-condition */ - assertTrue(DATABASE_3_PUBLIC); - - /* mock */ - when(userRepository.findByUsername(USER_1_USERNAME)) - .thenReturn(Optional.of(USER_1)); - - /* test */ - count_generic(DATABASE_3_ID, List.of(DATABASE_3), USER_1_PRINCIPAL, USER_1_ID, "access"); - } - - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"list-databases"}) - public void count_hasRoleForeign_succeeds() throws UserNotFoundException { - - /* pre-condition */ - assertTrue(DATABASE_3_PUBLIC); - - /* mock */ - when(userRepository.findByUsername(USER_1_USERNAME)) - .thenReturn(Optional.of(USER_1)); - - /* test */ - count_generic(DATABASE_3_ID, List.of(DATABASE_3), USER_1_PRINCIPAL, USER_1_ID, "access"); - } - @Test @WithAnonymousUser public void visibility_anonymous_fails() { @@ -396,8 +355,8 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void findById_anonymous_succeeds() throws NotAllowedException, DatabaseNotFoundException, - ExchangeNotFoundException, BrokerRemoteException { + public void findById_anonymous_succeeds() throws DatabaseNotFoundException, ExchangeNotFoundException, + BrokerRemoteException { /* test */ findById_generic(DATABASE_1_ID, DATABASE_1, null); @@ -415,8 +374,8 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) - public void findById_hasRole_succeeds() throws NotAllowedException, DatabaseNotFoundException, - ExchangeNotFoundException, BrokerRemoteException { + public void findById_hasRole_succeeds() throws DatabaseNotFoundException, ExchangeNotFoundException, + BrokerRemoteException { /* pre-condition */ assertTrue(DATABASE_3_PUBLIC); @@ -427,8 +386,8 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) - public void findById_hasRoleForeign_succeeds() throws NotAllowedException, DatabaseNotFoundException, - ExchangeNotFoundException, BrokerRemoteException { + public void findById_hasRoleForeign_succeeds() throws DatabaseNotFoundException, ExchangeNotFoundException, + BrokerRemoteException { /* pre-condition */ assertTrue(DATABASE_3_PUBLIC); @@ -439,8 +398,8 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) - public void findById_ownerSeesAccessRights_succeeds() throws NotAllowedException, DatabaseNotFoundException, - ExchangeNotFoundException, BrokerRemoteException { + public void findById_ownerSeesAccessRights_succeeds() throws DatabaseNotFoundException, ExchangeNotFoundException, + BrokerRemoteException { /* mock */ when(accessService.list(DATABASE_1_ID)) @@ -475,36 +434,11 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { assertEquals(databases.size(), body.size()); } - public void count_generic(Long databaseId, List<Database> databases, Principal principal, UUID userId, - String filter) throws UserNotFoundException { - - /* mock */ - when(identifierRepository.findByDatabaseId(databaseId)) - .thenReturn(List.of()); - if (principal != null) { - when(databaseService.findAccess(userId)) - .thenReturn(databases); - } else { - when(databaseService.findAll()) - .thenReturn(databases); - } - - /* test */ - final ResponseEntity<List<DatabaseDto>> response = databaseEndpoint.count(principal, filter); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNull(response.getBody()); - final List<String> headerCount = response.getHeaders().get("x-count"); - assertNotNull(headerCount); - assertEquals(headerCount.size(), 1); - assertEquals(headerCount.get(0), "" + databases.size()); - } - public void create_generic(Long databaseId, DatabaseCreateDto data, String username, - Principal principal) throws UserNotFoundException, DatabaseNameExistsException, - NotAllowedException, ContainerConnectionException, DatabaseMalformedException, QueryStoreException, - DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, - AmqpException, BrokerVirtualHostModificationException, ContainerNotFoundException, - BrokerVirtualHostGrantException, KeycloakRemoteException, AccessDeniedException, BrokerRemoteException { + Principal principal) throws UserNotFoundException, NotAllowedException, + DatabaseMalformedException, QueryStoreException, DatabaseConnectionException, QueryMalformedException, + DatabaseNotFoundException, ContainerNotFoundException, BrokerVirtualHostGrantException, + BrokerRemoteException { /* mock */ doNothing() @@ -515,14 +449,14 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { .setVirtualHostPermissions(username); /* test */ - final ResponseEntity<DatabaseBriefDto> response = databaseEndpoint.create(data, principal); + final ResponseEntity<DatabaseDto> response = databaseEndpoint.create(data, principal); assertEquals(HttpStatus.CREATED, response.getStatusCode()); assertNotNull(response.getBody()); } public void visibility_generic(Long databaseId, Database database, DatabaseDto dto, DatabaseModifyVisibilityDto data, Principal principal) throws NotAllowedException, - DatabaseNotFoundException, UserNotFoundException { + DatabaseNotFoundException { /* mock */ if (database != null) { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/QueryEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/QueryEndpointUnitTest.java index 3ca4cb4df0..2ae8b04359 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/QueryEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/QueryEndpointUnitTest.java @@ -13,6 +13,7 @@ import at.tuwien.querystore.Query; import at.tuwien.repository.mdb.DatabaseRepository; import at.tuwien.service.QueryService; import at.tuwien.service.StoreService; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.log4j.Log4j2; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Test; @@ -34,8 +35,7 @@ import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @Log4j2 @SpringBootTest @@ -168,7 +168,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ generic_reExecute(DATABASE_3_ID, QUERY_4_ID, QUERY_4, QUERY_4_RESULT_ID, QUERY_4_RESULT_DTO, - null, DATABASE_3); + null, DATABASE_3, true); } @Test @@ -179,7 +179,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ generic_reExecute(DATABASE_3_ID, QUERY_4_ID, QUERY_4, QUERY_4_RESULT_ID, QUERY_4_RESULT_DTO, - USER_2_PRINCIPAL, DATABASE_3); + USER_2_PRINCIPAL, DATABASE_3, true); } @Test @@ -190,7 +190,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ generic_reExecute(DATABASE_3_ID, QUERY_4_ID, QUERY_4, QUERY_4_RESULT_ID, QUERY_4_RESULT_DTO, - USER_2_PRINCIPAL, DATABASE_3); + USER_2_PRINCIPAL, DATABASE_3, true); } @Test @@ -319,7 +319,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { generic_reExecute(DATABASE_2_ID, QUERY_1_ID, QUERY_1, QUERY_1_RESULT_ID, QUERY_1_RESULT_DTO, - null, DATABASE_2); + null, DATABASE_2, true); }); } @@ -334,7 +334,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ generic_reExecute(DATABASE_2_ID, QUERY_1_ID, QUERY_1, QUERY_1_RESULT_ID, QUERY_1_RESULT_DTO, - USER_2_PRINCIPAL, DATABASE_2); + USER_2_PRINCIPAL, DATABASE_2, true); } @Test @@ -348,7 +348,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ generic_reExecute(DATABASE_2_ID, QUERY_1_ID, QUERY_1, QUERY_1_RESULT_ID, QUERY_1_RESULT_DTO, - USER_2_PRINCIPAL, DATABASE_2); + USER_2_PRINCIPAL, DATABASE_2, true); } @Test @@ -363,7 +363,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ generic_reExecute(DATABASE_2_ID, QUERY_1_ID, QUERY_1, QUERY_1_RESULT_ID, QUERY_1_RESULT_DTO, - USER_2_PRINCIPAL, DATABASE_2); + USER_2_PRINCIPAL, DATABASE_2, true); } @Test @@ -447,13 +447,12 @@ public class QueryEndpointUnitTest extends BaseUnitTest { assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals(QUERY_1_RESULT_ID, response.getBody().getId()); - assertEquals(QUERY_1_RESULT_NUMBER, response.getBody().getResultNumber()); assertEquals(QUERY_1_RESULT_NUMBER, response.getBody().getResult().size()); assertEquals(QUERY_1_RESULT_RESULT, response.getBody().getResult()); } protected void generic_reExecute(Long databaseId, Long queryId, Query query, Long resultId, - QueryResultDto result, Principal principal, Database database) + QueryResultDto result, Principal principal, Database database, boolean isGet) throws QueryStoreException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, TableMalformedException, QueryMalformedException, ColumnParseException, SortException, NotAllowedException, PaginationException, at.tuwien.exception.AccessDeniedException { @@ -469,11 +468,14 @@ public class QueryEndpointUnitTest extends BaseUnitTest { .thenReturn(query); when(queryService.reExecute(databaseId, query, page, size, sortDirection, sortColumn, principal)) .thenReturn(result); + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()) + .thenReturn(isGet ? "GET" : "HEAD"); /* test */ final ResponseEntity<QueryResultDto> response = queryEndpoint.reExecute(databaseId, queryId, - principal, page, size, sortDirection, sortColumn); - assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + principal, request, page, size, sortDirection, sortColumn); + assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals(resultId, response.getBody().getId()); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableDataEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableDataEndpointUnitTest.java index bdab17056d..7061b6a251 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableDataEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableDataEndpointUnitTest.java @@ -16,6 +16,7 @@ import at.tuwien.service.AccessService; import at.tuwien.service.DatabaseService; import at.tuwien.service.TableService; import at.tuwien.service.impl.QueryServiceImpl; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,6 +39,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @Log4j2 @@ -259,8 +261,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, null, - 3L, null, null); + generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, null, 3L, null, null, true); }); } @@ -270,8 +271,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, 3L, - null, null, null); + generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, 3L, null, null, null, true); }); } @@ -281,8 +281,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, -3L, - 3L, null, null); + generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, -3L, 3L, null, null, true); }); } @@ -292,8 +291,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, 3L, - -3L, null, null); + generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, 3L, -3L, null, null, true); }); } @@ -303,8 +301,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, 0L, - 0L, null, null); + generic_getAll(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, null, null, null, null, 0L, 0L, null, null, true); }); } @@ -314,7 +311,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - generic_getAll(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, null, null, null, null, null, null, null, null); + generic_getAll(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, null, null, null, null, null, null, null, null, true); }); } @@ -324,7 +321,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - generic_getAll(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_4_ID, DATABASE_1_USER_1_READ_ACCESS, USER_4_PRINCIPAL, null, null, null, null, null); + generic_getAll(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_4_ID, DATABASE_1_USER_1_READ_ACCESS, USER_4_PRINCIPAL, null, null, null, null, null, true); }); } @@ -334,7 +331,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - generic_getCount(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_4_ID, DATABASE_1_USER_1_READ_ACCESS, USER_4_PRINCIPAL, null); + generic_getAll(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_4_ID, DATABASE_1_USER_1_READ_ACCESS, USER_4_PRINCIPAL, null, null, null, null, null, false); }); } @@ -371,11 +368,11 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { DatabaseAccess access, Principal principal, Instant timestamp, Long page, Long size, SortType sortDirection, String sortColumn) throws TableNotFoundException, SortException, TableMalformedException, NotAllowedException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, PaginationException, AccessDeniedException { + ImageNotSupportedException, PaginationException, AccessDeniedException, QueryStoreException { /* test */ generic_getAll(databaseId, tableId, database, table, userId, access, principal, timestamp, - page, size, sortDirection, sortColumn); + page, size, sortDirection, sortColumn, true); } public static Stream<Arguments> getCount_succeeds_parameters() { @@ -409,10 +406,11 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { public void getCount_succeeds(String test, Long databaseId, Long tableId, Database database, Table table, UUID userId, DatabaseAccess access, Principal principal, Instant timestamp) throws TableNotFoundException, QueryStoreException, TableMalformedException, NotAllowedException, - QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, AccessDeniedException { + QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, AccessDeniedException, + SortException, PaginationException { /* test */ - generic_getCount(databaseId, tableId, database, table, userId, access, principal, timestamp); + generic_getAll(databaseId, tableId, database, table, userId, access, principal, timestamp, null, null, null, null, false); } @@ -461,9 +459,9 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { public void generic_getAll(Long databaseId, Long tableId, Database database, Table table, UUID userId, DatabaseAccess access, Principal principal, Instant timestamp, Long page, Long size, - SortType sortDirection, String sortColumn) throws TableMalformedException, + SortType sortDirection, String sortColumn, boolean isGet) throws TableMalformedException, NotAllowedException, PaginationException, TableNotFoundException, SortException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, AccessDeniedException { + DatabaseNotFoundException, ImageNotSupportedException, AccessDeniedException, QueryStoreException { /* mock */ when(databaseService.find(databaseId)) @@ -474,38 +472,25 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { .thenReturn(access); when(queryService.tableFindAll(eq(databaseId), eq(tableId), eq(timestamp), anyLong(), anyLong(), eq(principal))) .thenReturn(QUERY_1_RESULT_DTO); - - /* test */ - final ResponseEntity<QueryResultDto> response = dataEndpoint.getAll(databaseId, tableId, - principal, timestamp, page, size, sortDirection, sortColumn); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertEquals(QUERY_1_RESULT_ID, response.getBody().getId()); - assertEquals(QUERY_1_RESULT_NUMBER, response.getBody().getResultNumber()); - assertEquals(QUERY_1_RESULT_RESULT, response.getBody().getResult()); - } - - public void generic_getCount(Long databaseId, Long tableId, Database database, Table table, UUID userId, - DatabaseAccess access, Principal principal, Instant timestamp) - throws TableMalformedException, NotAllowedException, TableNotFoundException, QueryStoreException, - QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, AccessDeniedException { - - /* mock */ - when(databaseService.find(databaseId)) - .thenReturn(database); - when(tableService.find(databaseId, tableId)) - .thenReturn(table); - when(accessService.find(databaseId, userId)) - .thenReturn(access); - when(queryService.tableCount(databaseId, tableId, timestamp, principal)) + when(queryService.tableCount(eq(databaseId), eq(tableId), eq(timestamp), eq(principal))) .thenReturn(QUERY_1_RESULT_NUMBER); + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()) + .thenReturn(isGet ? "GET" : "HEAD"); /* test */ - final ResponseEntity<Long> response = dataEndpoint.getCount(databaseId, tableId, - principal, timestamp); + final ResponseEntity<QueryResultDto> response = dataEndpoint.getAll(databaseId, tableId, + principal, request, timestamp, page, size, sortDirection, sortColumn); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertEquals(QUERY_1_RESULT_NUMBER, response.getBody()); + assertNotNull(response.getHeaders().get("X-Count")); + assertEquals(1, response.getHeaders().get("X-Count").size()); + assertEquals(QUERY_1_RESULT_NUMBER, Long.parseLong(response.getHeaders().get("X-Count").get(0))); + if (isGet) { + assertNotNull(response.getBody()); + assertEquals(QUERY_1_RESULT_ID, response.getBody().getId()); + assertEquals(QUERY_1_RESULT_NUMBER, response.getBody().getResult().size()); + assertEquals(QUERY_1_RESULT_RESULT, response.getBody().getResult()); + } } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java index f07af86a82..a7ac48a931 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java @@ -536,10 +536,11 @@ public class TableEndpointUnitTest extends BaseUnitTest { return tableEndpoint.list(databaseId, principal); } - protected ResponseEntity<TableBriefDto> generic_create(Long databaseId, Database database, TableCreateDto data, + protected ResponseEntity<TableDto> generic_create(Long databaseId, Database database, TableCreateDto data, UUID userId, Principal principal, DatabaseAccess access) throws DatabaseNotFoundException, NotAllowedException, TableMalformedException, QueryMalformedException, - ImageNotSupportedException, TableNameExistsException, AccessDeniedException, TableNotFoundException { + ImageNotSupportedException, TableNameExistsException, AccessDeniedException, TableNotFoundException, + UserNotFoundException { /* mock */ if (database != null) { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointUnitTest.java index c4dc78327d..7fd281d420 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointUnitTest.java @@ -15,6 +15,7 @@ import at.tuwien.service.AccessService; import at.tuwien.service.DatabaseService; import at.tuwien.service.QueryService; import at.tuwien.service.ViewService; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -209,45 +210,41 @@ public class ViewEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void data_publicAnonymous_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, DatabaseConnectionException, QueryMalformedException, - QueryStoreException, TableMalformedException, ColumnParseException, ImageNotSupportedException, - ContainerNotFoundException, PaginationException, ViewMalformedException, AccessDeniedException { + DatabaseNotFoundException, ViewNotFoundException, QueryMalformedException, QueryStoreException, + TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ - data_generic(DATABASE_3_ID, VIEW_1_ID, DATABASE_3, null, null, null); + data_generic(VIEW_1_ID, VIEW_1, DATABASE_3_ID, DATABASE_3, null, null, null, true); } @Test @WithMockUser(username = USER_2_USERNAME) public void data_publicNoRole_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, DatabaseConnectionException, QueryMalformedException, - QueryStoreException, TableMalformedException, ColumnParseException, ImageNotSupportedException, - ContainerNotFoundException, PaginationException, ViewMalformedException, AccessDeniedException { + DatabaseNotFoundException, ViewNotFoundException, QueryMalformedException, QueryStoreException, + TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ - data_generic(DATABASE_3_ID, VIEW_1_ID, DATABASE_3, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); + data_generic(VIEW_1_ID, VIEW_1, DATABASE_3_ID, DATABASE_3, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS, true); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"view-database-view-data"}) public void data_publicHasRole_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, DatabaseConnectionException, QueryMalformedException, - QueryStoreException, TableMalformedException, ColumnParseException, ImageNotSupportedException, - ContainerNotFoundException, PaginationException, ViewMalformedException, AccessDeniedException { + DatabaseNotFoundException, ViewNotFoundException, QueryMalformedException, QueryStoreException, + TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ - data_generic(DATABASE_3_ID, VIEW_1_ID, DATABASE_3, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); + data_generic(VIEW_1_ID, VIEW_1, DATABASE_3_ID, DATABASE_3, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS, true); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"view-database-view-data"}) public void data_publicHasRoleHasAccess_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, DatabaseConnectionException, QueryMalformedException, - QueryStoreException, TableMalformedException, ColumnParseException, ImageNotSupportedException, - ContainerNotFoundException, PaginationException, ViewMalformedException, AccessDeniedException { + DatabaseNotFoundException, ViewNotFoundException, QueryMalformedException, QueryStoreException, + TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ - data_generic(DATABASE_3_ID, VIEW_1_ID, DATABASE_3, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); + data_generic(VIEW_1_ID, VIEW_1, DATABASE_3_ID, DATABASE_3, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS, true); } /* ################################################################################################### */ @@ -328,8 +325,8 @@ public class ViewEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_privateAnonymous_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, AccessDeniedException { + public void find_privateAnonymous_succeeds() throws UserNotFoundException, DatabaseNotFoundException, + ViewNotFoundException, AccessDeniedException { /* test */ find_generic(DATABASE_1_ID, VIEW_1_ID, DATABASE_1, null, null, null); @@ -337,8 +334,8 @@ public class ViewEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"find-database-view"}) - public void find_privateHasRole_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, AccessDeniedException { + public void find_privateHasRole_succeeds() throws UserNotFoundException, DatabaseNotFoundException, + ViewNotFoundException, AccessDeniedException { /* test */ find_generic(DATABASE_1_ID, VIEW_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); @@ -346,8 +343,8 @@ public class ViewEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_2_USERNAME) - public void find_privateNoRole_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, AccessDeniedException { + public void find_privateNoRole_succeeds() throws UserNotFoundException, DatabaseNotFoundException, + ViewNotFoundException, AccessDeniedException { /* test */ find_generic(DATABASE_1_ID, VIEW_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); @@ -355,8 +352,8 @@ public class ViewEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_2_USERNAME) - public void find_privateHasRoleHasAccess_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, AccessDeniedException { + public void find_privateHasRoleHasAccess_succeeds() throws UserNotFoundException, DatabaseNotFoundException, + ViewNotFoundException, AccessDeniedException { /* test */ find_generic(DATABASE_1_ID, VIEW_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); @@ -408,51 +405,48 @@ public class ViewEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - data_generic(DATABASE_1_ID, VIEW_1_ID, DATABASE_1, null, null, null); + data_generic(VIEW_1_ID, VIEW_1, DATABASE_1_ID, DATABASE_1, null, null, null, true); }); } @Test @WithMockUser(username = USER_2_USERNAME) public void data_privateNoRole_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, DatabaseConnectionException, QueryMalformedException, - QueryStoreException, TableMalformedException, ColumnParseException, ImageNotSupportedException, - ContainerNotFoundException, PaginationException, ViewMalformedException, AccessDeniedException { + DatabaseNotFoundException, ViewNotFoundException, QueryMalformedException, QueryStoreException, + TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ - data_generic(DATABASE_1_ID, VIEW_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); + data_generic(VIEW_1_ID, VIEW_1, DATABASE_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS, true); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"view-database-view-data"}) public void data_privateHasRole_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, DatabaseConnectionException, QueryMalformedException, - QueryStoreException, TableMalformedException, ColumnParseException, ImageNotSupportedException, - ContainerNotFoundException, PaginationException, ViewMalformedException, AccessDeniedException { + DatabaseNotFoundException, ViewNotFoundException, QueryMalformedException, QueryStoreException, + TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ - data_generic(DATABASE_1_ID, VIEW_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); + data_generic(VIEW_1_ID, VIEW_1, DATABASE_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS, true); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"view-database-view-data"}) public void data_privateHasRoleHasAccess_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseNotFoundException, ViewNotFoundException, DatabaseConnectionException, QueryMalformedException, - QueryStoreException, TableMalformedException, ColumnParseException, ImageNotSupportedException, - ContainerNotFoundException, PaginationException, ViewMalformedException, AccessDeniedException { + DatabaseNotFoundException, ViewNotFoundException, QueryMalformedException, QueryStoreException, + TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ - data_generic(DATABASE_1_ID, VIEW_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS); + data_generic(VIEW_1_ID, VIEW_1, DATABASE_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2_USER_1_READ_ACCESS, true); } @Test @WithAnonymousUser - public void count_privateAnonymous_succeeds() throws UserNotFoundException, DatabaseNotFoundException, - ViewNotFoundException, DatabaseConnectionException, QueryMalformedException, QueryStoreException, - TableMalformedException, ImageNotSupportedException, ContainerNotFoundException { + public void count_privateAnonymous_succeeds() throws UserNotFoundException, NotAllowedException, + DatabaseNotFoundException, ViewNotFoundException, QueryMalformedException, QueryStoreException, + TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ - count_generic(DATABASE_1_ID, VIEW_2_ID, DATABASE_1, VIEW_2, USER_2_PRINCIPAL); + data_generic(VIEW_2_ID, VIEW_2, DATABASE_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, null, false); } @Test @@ -461,7 +455,7 @@ public class ViewEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(DatabaseNotFoundException.class, () -> { - count_generic(DATABASE_1_ID, VIEW_2_ID, null, VIEW_2, USER_2_PRINCIPAL); + data_generic(VIEW_2_ID, VIEW_2, DATABASE_1_ID, null, USER_2_ID, USER_2_PRINCIPAL, null, false); }); } @@ -471,7 +465,7 @@ public class ViewEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(ViewNotFoundException.class, () -> { - count_generic(DATABASE_1_ID, VIEW_2_ID, DATABASE_1, null, USER_2_PRINCIPAL); + data_generic(VIEW_2_ID, null, DATABASE_1_ID, DATABASE_1, USER_2_ID, USER_2_PRINCIPAL, null, false); }); } @@ -546,7 +540,7 @@ public class ViewEndpointUnitTest extends BaseUnitTest { protected void find_generic(Long databaseId, Long viewId, Database database, UUID userId, Principal principal, DatabaseAccess access) throws DatabaseNotFoundException, - UserNotFoundException, NotAllowedException, ViewNotFoundException, AccessDeniedException { + UserNotFoundException, ViewNotFoundException, AccessDeniedException { /* mock */ when(databaseService.find(databaseId)) @@ -597,18 +591,25 @@ public class ViewEndpointUnitTest extends BaseUnitTest { assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); } - protected void data_generic(Long databaseId, Long viewId, Database database, UUID userId, - Principal principal, DatabaseAccess access) throws DatabaseNotFoundException, - UserNotFoundException, NotAllowedException, ViewNotFoundException, DatabaseConnectionException, - QueryMalformedException, QueryStoreException, TableMalformedException, ColumnParseException, - ImageNotSupportedException, ContainerNotFoundException, PaginationException, ViewMalformedException, + protected void data_generic(Long viewId, View view, Long databaseId, Database database, UUID userId, Principal principal, + DatabaseAccess access, boolean isGet) throws DatabaseNotFoundException, + UserNotFoundException, NotAllowedException, ViewNotFoundException, QueryMalformedException, + QueryStoreException, TableMalformedException, ImageNotSupportedException, PaginationException, AccessDeniedException { final Long page = 0L; final Long size = 2L; /* mock */ - when(databaseService.find(databaseId)) - .thenReturn(database); + if (database != null) { + log.trace("mock database with id {}", databaseId); + when(databaseService.find(databaseId)) + .thenReturn(database); + } else { + log.trace("mock no database with id {}", databaseId); + doThrow(DatabaseNotFoundException.class) + .when(databaseService) + .find(databaseId); + } if (access != null) { log.trace("mock access of database with id {} and user id {}", databaseId, userId); when(accessService.find(databaseId, userId)) @@ -618,50 +619,36 @@ public class ViewEndpointUnitTest extends BaseUnitTest { when(accessService.find(databaseId, userId)) .thenThrow(AccessDeniedException.class); } - when(viewService.findById(databaseId, viewId, principal)) - .thenReturn(VIEW_1); - when(queryService.viewFindAll(databaseId, VIEW_1, page, size, principal)) - .thenReturn(QUERY_1_RESULT_DTO); - - /* test */ - final ResponseEntity<QueryResultDto> response = viewEndpoint.data(databaseId, viewId, principal, page, size); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertEquals(QUERY_1_RESULT_ID, response.getBody().getId()); - assertEquals(QUERY_1_RESULT_NUMBER, response.getBody().getResultNumber()); - assertEquals(QUERY_1_RESULT_DTO, response.getBody()); - } - - protected void count_generic(Long databaseId, Long viewId, Database database, View view, - Principal principal) throws UserNotFoundException, QueryStoreException, - DatabaseConnectionException, TableMalformedException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, ContainerNotFoundException, ViewNotFoundException { - - /* mock */ - if (database != null) { - when(databaseService.find(databaseId)) - .thenReturn(database); - } else { - doThrow(DatabaseNotFoundException.class) - .when(databaseService) - .find(databaseId); - } if (view != null) { + log.trace("mock view with id {}", viewId); when(viewService.findById(databaseId, viewId, principal)) - .thenReturn(VIEW_1); + .thenReturn(view); } else { + log.trace("mock no view with id {}", viewId); doThrow(ViewNotFoundException.class) .when(viewService) .findById(databaseId, viewId, principal); } - when(queryService.viewCount(databaseId, VIEW_1, principal)) - .thenReturn(5L); + when(queryService.viewFindAll(databaseId, VIEW_1, page, size, principal)) + .thenReturn(QUERY_1_RESULT_DTO); + when(queryService.viewCount(eq(databaseId), any(View.class), eq(principal))) + .thenReturn(QUERY_1_RESULT_NUMBER); + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()) + .thenReturn(isGet ? "GET" : "HEAD"); /* test */ - final ResponseEntity<Long> response = viewEndpoint.count(databaseId, viewId, principal); + final ResponseEntity<QueryResultDto> response = viewEndpoint.data(databaseId, viewId, principal, request, page, size); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertEquals(5L, response.getBody()); + assertNotNull(response.getHeaders().get("X-Count")); + assertNotNull(response.getHeaders().get("X-Count").get(0)); + assertEquals(QUERY_1_RESULT_NUMBER, Long.parseLong(response.getHeaders().get("X-Count").get(0))); + if (isGet) { + assertNotNull(response.getBody()); + assertEquals(QUERY_1_RESULT_ID, response.getBody().getId()); + assertEquals(QUERY_1_RESULT_NUMBER, response.getBody().getResult().size()); + assertEquals(QUERY_1_RESULT_DTO, response.getBody()); + } } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java index ce0b3a3e57..d45d641f7c 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java @@ -207,11 +207,6 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { } catch (Exception e) { /* ignore */ } - try { - databaseEndpoint.count(USER_1_PRINCIPAL, null); - } catch (Exception e) { - /* ignore */ - } try { databaseEndpoint.create(DATABASE_1_CREATE, USER_1_PRINCIPAL); } catch (Exception e) { @@ -239,7 +234,7 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { } /* test */ - for (String metric : List.of("dbr_database_findall", "dbr_database_count", "dbr_database_create", "dbr_database_visibility", "dbr_database_transfer", "dbr_database_find", "dbr_database_image")) { + for (String metric : List.of("dbr_database_findall", "dbr_database_create", "dbr_database_visibility", "dbr_database_transfer", "dbr_database_find", "dbr_database_image")) { assertThat(registry) .hasObservationWithNameEqualTo(metric); } @@ -488,12 +483,7 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { /* ignore */ } try { - queryEndpoint.reExecute(DATABASE_1_ID, QUERY_1_ID, USER_1_PRINCIPAL, null, null, null, null); - } catch (Exception e) { - /* ignore */ - } - try { - queryEndpoint.reExecuteCount(DATABASE_1_ID, QUERY_1_ID, USER_1_PRINCIPAL); + queryEndpoint.reExecute(DATABASE_1_ID, QUERY_1_ID, USER_1_PRINCIPAL, null, null, null, null, null); } catch (Exception e) { /* ignore */ } @@ -504,7 +494,7 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { } /* test */ - for (String metric : List.of("dbr_query_execute", "dbr_query_reexecute", "dbr_query_reexecute_count", "dbr_query_export")) { + for (String metric : List.of("dbr_query_execute", "dbr_query_reexecute", "dbr_query_export")) { assertThat(registry) .hasObservationWithNameEqualTo(metric); } @@ -617,18 +607,13 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { /* ignore */ } try { - tableDataEndpoint.getAll(DATABASE_1_ID, TABLE_1_ID, USER_1_PRINCIPAL, null, null, null, null, null); - } catch (Exception e) { - /* ignore */ - } - try { - tableDataEndpoint.getCount(DATABASE_1_ID, TABLE_1_ID, USER_1_PRINCIPAL, null); + tableDataEndpoint.getAll(DATABASE_1_ID, TABLE_1_ID, USER_1_PRINCIPAL, null, null, null, null, null, null); } catch (Exception e) { /* ignore */ } /* test */ - for (String metric : List.of("dbr_table_data_insert", "dbr_table_data_update", "dbr_table_data_delete", "dbr_table_data_import", "dbr_table_data_findall", "dbr_table_data_countall")) { + for (String metric : List.of("dbr_table_data_insert", "dbr_table_data_update", "dbr_table_data_delete", "dbr_table_data_import", "dbr_table_data_findall")) { assertThat(registry) .hasObservationWithNameEqualTo(metric); } @@ -763,18 +748,13 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { /* ignore */ } try { - viewEndpoint.data(DATABASE_1_ID, VIEW_1_ID, USER_1_PRINCIPAL, null, null); - } catch (Exception e) { - /* ignore */ - } - try { - viewEndpoint.count(DATABASE_1_ID, VIEW_1_ID, USER_1_PRINCIPAL); + viewEndpoint.data(DATABASE_1_ID, VIEW_1_ID, USER_1_PRINCIPAL, null, null, null); } catch (Exception e) { /* ignore */ } /* test */ - for (String metric : List.of("dbr_views_findall", "dbr_view_create", "dbr_view_find", "dbr_view_delete", "dbr_view_data_findall", "dbr_view_data_count")) { + for (String metric : List.of("dbr_views_findall", "dbr_view_create", "dbr_view_find", "dbr_view_delete", "dbr_view_data_findall")) { assertThat(registry) .hasObservationWithNameEqualTo(metric); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java index 80a555b9c3..fa57f4c630 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java @@ -476,6 +476,19 @@ public class DatabaseServiceIntegrationTest extends BaseUnitTest { final Database response = databaseService.create(createDto, USER_1_PRINCIPAL); assertEquals(database.getName(), response.getName()); assertTrue(response.getInternalName().startsWith(database.getInternalName())); + assertNotNull(response.getContainer()); + assertNotNull(response.getTables()); + assertNotNull(response.getViews()); + assertNotNull(response.getAccesses()); + assertNotNull(response.getAccesses()); + assertNotNull(response.getIdentifiers()); + assertNotNull(response.getSubsets()); + assertNotNull(response.getCreator()); + assertNotNull(response.getContact()); + assertNotNull(response.getOwner()); + assertNull(response.getImage()); + assertNotNull(response.getExchangeName()); + assertEquals(database.getIsPublic(), response.getIsPublic()); return response; } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java index 4eea12c7f0..a0f93d19f4 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java @@ -48,7 +48,7 @@ public class DatabaseServiceUnitTest extends BaseUnitTest { @Test public void findAll_succeeds() { /* mock */ - when(databaseRepository.findAll()) + when(databaseRepository.findAllDesc()) .thenReturn(List.of(DATABASE_1)); /* test */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceIntegrationTest.java index a82c25a658..f72b713619 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceIntegrationTest.java @@ -205,39 +205,6 @@ public class IdentifierServiceIntegrationTest extends BaseUnitTest { assertEquals(RELATED_IDENTIFIER_5_TYPE, relatedIdentifier1.getType()); assertEquals(RELATED_IDENTIFIER_5_RELATION_TYPE, relatedIdentifier1.getRelation()); assertEquals(RELATED_IDENTIFIER_5_VALUE, relatedIdentifier1.getValue()); - /* open search database */ - final Optional<DatabaseDto> optional = databaseIdxRepository.findById(IDENTIFIER_5_DATABASE_ID); - assertTrue(optional.isPresent()); - assertNotNull(optional.get().getIdentifiers()); - assertEquals(2, optional.get().getIdentifiers().size()); - final IdentifierDto dto1 = optional.get().getIdentifiers().get(1); - assertNotNull(dto1.getTitles()); - assertEquals(1, dto1.getTitles().size()); - final IdentifierTitleDto titleDto0 = dto1.getTitles().get(0); - assertEquals(IDENTIFIER_5_TITLE_1_TITLE, titleDto0.getTitle()); - assertEquals(IDENTIFIER_5_TITLE_1_LANG_DTO, titleDto0.getLanguage()); - assertEquals(IDENTIFIER_5_TITLE_1_TYPE_DTO, titleDto0.getTitleType()); - assertNotNull(dto1.getDescriptions()); - assertEquals(1, dto1.getDescriptions().size()); - final IdentifierDescriptionDto descriptionDto0 = dto1.getDescriptions().get(0); - assertEquals(IDENTIFIER_5_DESCRIPTION_1_DESCRIPTION, descriptionDto0.getDescription()); - assertEquals(IDENTIFIER_5_DESCRIPTION_1_LANG_DTO, descriptionDto0.getLanguage()); - assertEquals(IDENTIFIER_5_DESCRIPTION_1_TYPE_DTO, descriptionDto0.getDescriptionType()); - assertNull(dto1.getDoi()); - assertEquals(IDENTIFIER_5_PUBLISHER, dto1.getPublisher()); - assertNull(dto1.getLanguage()); - assertEquals(IDENTIFIER_5_PUBLICATION_YEAR, dto1.getPublicationYear()); - assertEquals(IDENTIFIER_5_PUBLICATION_MONTH, dto1.getPublicationMonth()); - assertEquals(IDENTIFIER_5_PUBLICATION_DAY, dto1.getPublicationDay()); - final List<RelatedIdentifierDto> relatedIdentifiersDto = dto1.getRelatedIdentifiers(); - assertEquals(1, relatedIdentifiersDto.size()); - final RelatedIdentifierDto relatedIdentifierDto1 = relatedIdentifiersDto.get(0); - assertEquals(RELATED_IDENTIFIER_5_TYPE_DTO, relatedIdentifierDto1.getType()); - assertEquals(RELATED_IDENTIFIER_5_RELATION_TYPE_DTO, relatedIdentifierDto1.getRelation()); - assertEquals(RELATED_IDENTIFIER_5_VALUE, relatedIdentifierDto1.getValue()); - /* open search database */ - final Optional<DatabaseDto> responseDto = databaseIdxRepository.findById(DATABASE_2_ID); - assertTrue(responseDto.isPresent()); } @Test diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceUnitTest.java index f731e99d25..3a48cdc696 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceUnitTest.java @@ -9,10 +9,7 @@ import at.tuwien.api.orcid.OrcidDto; import at.tuwien.api.ror.RorDto; import at.tuwien.api.user.external.ExternalMetadataDto; import at.tuwien.api.user.external.affiliation.ExternalAffiliationDto; -import at.tuwien.exception.DoiNotFoundException; -import at.tuwien.exception.OrcidNotFoundException; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.RorNotFoundException; +import at.tuwien.exception.*; import at.tuwien.gateway.CrossrefGateway; import at.tuwien.gateway.OrcidGateway; import at.tuwien.gateway.RorGateway; @@ -62,8 +59,8 @@ public class MetadataServiceUnitTest extends BaseUnitTest { private ObjectMapper objectMapper; @Test - public void findByUrl_orcid_succeeds() throws OrcidNotFoundException, RemoteUnavailableException, - RorNotFoundException, IOException, DoiNotFoundException { + public void findByUrl_orcid_succeeds() throws OrcidNotFoundException, + RorNotFoundException, IOException, DoiNotFoundException, IdentifierNotFoundException { final OrcidDto orcid = objectMapper .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .readValue(new File("src/test/resources/json/orcid_jdoe.json"), OrcidDto.class); @@ -93,8 +90,8 @@ public class MetadataServiceUnitTest extends BaseUnitTest { } @Test - public void findByUrl_doi_succeeds() throws OrcidNotFoundException, RemoteUnavailableException, - RorNotFoundException, IOException, DoiNotFoundException { + public void findByUrl_doi_succeeds() throws OrcidNotFoundException, + RorNotFoundException, IOException, DoiNotFoundException, IdentifierNotFoundException { final CrossrefDto doi = objectMapper .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .readValue(new File("src/test/resources/json/doi_ec.json"), CrossrefDto.class); @@ -126,8 +123,8 @@ public class MetadataServiceUnitTest extends BaseUnitTest { } @Test - public void findByUrl_ror_succeeds() throws OrcidNotFoundException, RemoteUnavailableException, - RorNotFoundException, IOException, DoiNotFoundException { + public void findByUrl_ror_succeeds() throws OrcidNotFoundException, + RorNotFoundException, IOException, DoiNotFoundException, IdentifierNotFoundException { final RorDto ror = objectMapper .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .readValue(new File("src/test/resources/json/ror_tuw.json"), RorDto.class); @@ -170,7 +167,7 @@ public class MetadataServiceUnitTest extends BaseUnitTest { public void findByUrl_isniMalformed_fails() { /* test */ - assertThrows(RemoteUnavailableException.class, () -> { + assertThrows(IdentifierNotFoundException.class, () -> { metadataService.findByUrl("https://isni.org/isni/0000000506791090"); }); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java index eedba5e102..0b075bfe44 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java @@ -350,9 +350,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ final QueryResultDto response = queryService.execute(DATABASE_2_ID, request, USER_1_PRINCIPAL, 0L, 100L, null, null); - assertEquals(4L, response.getResultNumber()); assertNotNull(response.getResult()); final List<Map<String, Object>> result = response.getResult(); + assertEquals(4L, result.size()); assertEquals(BigInteger.valueOf(1L), result.get(0).get("id")); assertEquals(4, result.get(0).get("legs")); assertEquals("boar", result.get(0).get("animal_name")); @@ -393,9 +393,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ final QueryResultDto response = queryService.execute(DATABASE_1_ID, request, USER_1_PRINCIPAL, 0L, 100L, null, null); - assertEquals(1L, response.getResultNumber()); assertNotNull(response.getResult()); final List<Map<String, Object>> result = response.getResult(); + assertEquals(1L, result.size()); assertEquals("Vienna", result.get(0).get("location")); assertNull(result.get(0).get("lat")); assertNull(result.get(0).get("lng")); @@ -415,9 +415,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ final QueryResultDto response = queryService.execute(DATABASE_1_ID, request, USER_1_PRINCIPAL, 0L, 100L, null, null); - assertEquals(1L, response.getResultNumber()); assertNotNull(response.getResult()); final List<Map<String, Object>> result = response.getResult(); + assertEquals(1L, result.size()); assertEquals("Vienna", result.get(0).get("location")); assertNull(result.get(0).get("lat")); assertNull(result.get(0).get("lng")); @@ -437,7 +437,7 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ final QueryResultDto response = queryService.execute(DATABASE_1_ID, request, USER_1_PRINCIPAL, 0L, 100L, null, null); - assertEquals(1L, response.getResultNumber()); + assertEquals(1L, response.getResult().size()); assertNotNull(response.getResult()); } @@ -454,9 +454,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ final QueryResultDto response = queryService.execute(DATABASE_1_ID, request, USER_1_PRINCIPAL, 0L, 100L, null, null); - assertEquals(9L, response.getResultNumber()); assertNotNull(response.getResult()); final List<Map<String, Object>> result = response.getResult(); + assertEquals(9L, result.size()); assertEquals(2, result.get(0).keySet().size()); assertEquals("Albury", result.get(0).get("a")); assertEquals("Albury", result.get(0).get("location")); @@ -500,9 +500,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ final QueryResultDto response = queryService.execute(DATABASE_1_ID, request, USER_1_PRINCIPAL, 0L, 100L, null, null); - assertEquals(9L, response.getResultNumber()); assertNotNull(response.getResult()); final List<Map<String, Object>> result = response.getResult(); + assertEquals(9L, result.size()); assertEquals("Albury", result.get(0).get("a")); assertEquals("Albury", result.get(0).get("location")); assertEquals("Albury", result.get(1).get("a")); diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationWriteTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationWriteTest.java index f4b9bddcb8..2364ac38c2 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationWriteTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationWriteTest.java @@ -104,7 +104,7 @@ public class TableServiceIntegrationWriteTest extends BaseUnitTest { @Test public void create_withConstraints_succeeds() throws TableMalformedException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, TableNameExistsException, SQLException, - TableNotFoundException { + TableNotFoundException, UserNotFoundException { /* test */ tableService.createTable(DATABASE_1_ID, TABLE_5_CREATE_DTO, USER_1_PRINCIPAL); // table to reference @@ -191,7 +191,7 @@ public class TableServiceIntegrationWriteTest extends BaseUnitTest { @Test @Transactional public void delete_full_succeeds() throws TableNotFoundException, TableMalformedException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, TableNameExistsException { + DatabaseNotFoundException, ImageNotSupportedException, TableNameExistsException, UserNotFoundException { /* test */ final Table response = tableService.createTable(DATABASE_1_ID, TABLE_0_CREATE_DTO, USER_1_PRINCIPAL); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/IdentifierService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/IdentifierService.java index 457b9dc3e8..0af9bd13ff 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/IdentifierService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/IdentifierService.java @@ -91,8 +91,6 @@ public interface IdentifierService { * @return The created identifier from the metadata database if successful. * @throws QueryNotFoundException The query was not found in the data database. * @throws IdentifierRequestException The identifier requested could not be created. - * @throws RemoteUnavailableException The connection to the Query Store could not be established by - * the database connector. * @throws UserNotFoundException The user was not found in the metadata database. * @throws DatabaseNotFoundException The database was not found in the metadata database. * @throws ViewNotFoundException The view with id was not found. @@ -100,7 +98,7 @@ public interface IdentifierService { * @throws ImageNotSupportedException The image is not supported. */ Identifier create(IdentifierSaveDto data, Principal principal) throws QueryNotFoundException, - IdentifierRequestException, RemoteUnavailableException, UserNotFoundException, DatabaseNotFoundException, + IdentifierRequestException, UserNotFoundException, DatabaseNotFoundException, ViewNotFoundException, QueryStoreException, ImageNotSupportedException; /** diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/MetadataService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/MetadataService.java index b7b2d1de20..16688e66b1 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/MetadataService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/MetadataService.java @@ -52,11 +52,11 @@ public interface MetadataService { * * @param url The user identifier. * @return The user metadata. - * @throws OrcidNotFoundException The provided identifier is of ORCID type and does not exist. - * @throws RorNotFoundException The provided identifier is of ROR type and does not exist. - * @throws RemoteUnavailableException The remote service is not supported. - * @throws DoiNotFoundException The doi was not found. + * @throws OrcidNotFoundException The provided identifier is of ORCID type and does not exist. + * @throws RorNotFoundException The provided identifier is of ROR type and does not exist. + * @throws IdentifierNotFoundException The identifier is not supported. + * @throws DoiNotFoundException The doi was not found. */ ExternalMetadataDto findByUrl(String url) throws OrcidNotFoundException, RorNotFoundException, - RemoteUnavailableException, DoiNotFoundException; + DoiNotFoundException, IdentifierNotFoundException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java index 09f8012670..bf9a3ddee1 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java @@ -82,7 +82,7 @@ public interface TableService { */ Table createTable(Long databaseId, TableCreateDto createDto, Principal principal) throws ImageNotSupportedException, DatabaseNotFoundException, TableMalformedException, - TableNameExistsException, QueryMalformedException, TableNotFoundException; + TableNameExistsException, QueryMalformedException, TableNotFoundException, UserNotFoundException; /** * Deletes a table from the database in the metadata database and data database. diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java index 5f2a5f0a5d..89f503f0b6 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java @@ -58,6 +58,12 @@ public class ContainerServiceImpl implements ContainerService { .image(optional2.get()) .name(data.getName()) .internalName(containerMapper.containerToInternalContainerName(data.getName())) + .host(data.getHost()) + .port(data.getPort()) + .sidecarHost(data.getSidecarHost()) + .sidecarPort(data.getSidecarPort()) + .privilegedUsername(data.getPrivilegedUsername()) + .privilegedPassword(data.getPrivilegedPassword()) .build(); log.info("Created container with id {} in metadata database", container.getId()); return container; diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DataCiteIdentifierServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DataCiteIdentifierServiceImpl.java index 541e1fee8a..3c320dde3a 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DataCiteIdentifierServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DataCiteIdentifierServiceImpl.java @@ -83,7 +83,7 @@ public class DataCiteIdentifierServiceImpl implements IdentifierService { @Override @Transactional(rollbackFor = {Exception.class}) public Identifier create(IdentifierSaveDto data, Principal principal) throws QueryNotFoundException, - IdentifierRequestException, RemoteUnavailableException, UserNotFoundException, DatabaseNotFoundException, + IdentifierRequestException, UserNotFoundException, DatabaseNotFoundException, ViewNotFoundException, QueryStoreException, ImageNotSupportedException { final Identifier identifier = identifierService.create(data, principal); /* https://stackoverflow.com/questions/55090541/spring-data-jpa-lombok-unsupportedoperationexception-during-saving */ diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java index 528280b1a7..a4902100b9 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java @@ -154,7 +154,7 @@ public class IdentifierServiceImpl implements IdentifierService { @Override @Transactional public Identifier create(IdentifierSaveDto data, Principal principal) throws QueryNotFoundException, - IdentifierRequestException, RemoteUnavailableException, UserNotFoundException, DatabaseNotFoundException, + IdentifierRequestException, UserNotFoundException, DatabaseNotFoundException, ViewNotFoundException, QueryStoreException, ImageNotSupportedException { /* create identifier */ final Identifier entity = identifierMapper.identifierCreateDtoToIdentifier(data); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java index 3353721f47..faf1cc94f4 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java @@ -6,7 +6,9 @@ import at.tuwien.api.database.DatabaseTransferDto; import at.tuwien.config.QueryConfig; import at.tuwien.entities.container.Container; import at.tuwien.entities.container.image.ContainerImageDate; +import at.tuwien.entities.database.AccessType; import at.tuwien.entities.database.Database; +import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.database.View; import at.tuwien.entities.database.table.Table; import at.tuwien.entities.database.table.columns.TableColumn; @@ -28,6 +30,7 @@ import at.tuwien.service.*; import com.mchange.v2.c3p0.ComboPooledDataSource; import lombok.extern.log4j.Log4j2; import net.sf.jsqlparser.JSQLParserException; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -125,17 +128,30 @@ public class MariaDbServiceImpl extends HibernateConnector implements DatabaseSe @Override @Transactional - public Database create(DatabaseCreateDto createDto, Principal principal) throws ContainerNotFoundException, + public Database create(DatabaseCreateDto data, Principal principal) throws ContainerNotFoundException, DatabaseMalformedException, UserNotFoundException, QueryMalformedException { /* start the object */ - final Database database = databaseMapper.databaseCreateDtoToDatabase(createDto); - final Container container = containerService.find(database.getCid()); + final Container container = containerService.find(data.getCid()); final User owner = userService.findByUsername(principal.getName()); - database.setContainer(container); - database.setOwnedBy(owner.getId()); - database.setCreatedBy(owner.getId()); - database.setContactPerson(owner.getId()); - database.setExchangeName("dbrepo"); + final Database database = Database.builder() + .isPublic(data.getIsPublic()) + .name(data.getName()) + .internalName(databaseMapper.nameToInternalName(data.getName()) + "_" + RandomStringUtils.randomAlphabetic(4).toLowerCase()) + .cid(data.getCid()) + .container(container) + .ownedBy(owner.getId()) + .owner(owner) + .createdBy(owner.getId()) + .creator(owner) + .contactPerson(owner.getId()) + .contact(owner) + .exchangeName("dbrepo") + .tables(new LinkedList<>()) + .views(new LinkedList<>()) + .subsets(new LinkedList<>()) + .accesses(new LinkedList<>()) + .identifiers(new LinkedList<>()) + .build(); final ComboPooledDataSource dataSource = getPrivilegedDataSource(container.getImage(), container); try { final Connection connection = dataSource.getConnection(); @@ -342,9 +358,9 @@ public class MariaDbServiceImpl extends HibernateConnector implements DatabaseSe log.info("Enabled system-versioning for table with name {}", table.getInternalName()); } table.setConstraints(Constraints.builder() - .checks(new LinkedHashSet<>()) - .foreignKeys(new LinkedList<>()) - .uniques(new LinkedList<>()) + .checks(new LinkedHashSet<>()) + .foreignKeys(new LinkedList<>()) + .uniques(new LinkedList<>()) .build()); table.setProcessedConstraints(false); final PreparedStatement preparedStatement3 = tableMapper.tableToCreateHistoryViewRawQuery(connection, table); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MetadataServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MetadataServiceImpl.java index 44278be9df..63cc106117 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MetadataServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MetadataServiceImpl.java @@ -154,7 +154,7 @@ public class MetadataServiceImpl implements MetadataService { @Override public ExternalMetadataDto findByUrl(String url) throws OrcidNotFoundException, RorNotFoundException, - RemoteUnavailableException, DoiNotFoundException { + DoiNotFoundException, IdentifierNotFoundException { if (url.contains("orcid.org")) { final OrcidDto orcidDto = orcidGateway.findByUrl(url); return externalMapper.orcidDtoToExternalMetadataDto(orcidDto); @@ -178,7 +178,7 @@ public class MetadataServiceImpl implements MetadataService { return externalMapper.crossrefDtoToExternalMetadataDto(crossrefDto); } log.error("Failed to find metadata: unsupported identifier {}", url); - throw new RemoteUnavailableException("Failed to find metadata: unsupported identifier " + url); + throw new IdentifierNotFoundException("Failed to find metadata: unsupported identifier " + url); } } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java index a24d93868b..13a47c8188 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java @@ -94,7 +94,6 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService final String statement = queryMapper.queryToRawTimestampedQuery(query.getQuery(), query.getCreated(), true, page, size); final QueryResultDto dto = executeNonPersistent(databaseId, statement, columns); dto.setId(query.getId()); - dto.setResultNumber(query.getResultNumber()); return dto; } @@ -149,8 +148,8 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService } } - public Long executeCountNonPersistent(Long databaseId, String statement) - throws QueryMalformedException, TableMalformedException, DatabaseNotFoundException, QueryStoreException { + public Long executeCountNonPersistent(Long databaseId, String statement) throws QueryMalformedException, + TableMalformedException, DatabaseNotFoundException, QueryStoreException { /* find */ final Database database = databaseService.find(databaseId); /* run query */ diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java index 525383b115..7c37aae376 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java @@ -2,12 +2,10 @@ package at.tuwien.service.impl; import at.tuwien.api.database.table.TableCreateDto; import at.tuwien.api.database.table.TableHistoryDto; -import at.tuwien.api.database.table.columns.concepts.ColumnSemanticsUpdateDto; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.table.Table; -import at.tuwien.entities.database.table.columns.TableColumn; -import at.tuwien.entities.database.table.columns.TableColumnConcept; -import at.tuwien.entities.database.table.columns.TableColumnUnit; +import at.tuwien.entities.database.table.constraints.Constraints; +import at.tuwien.entities.user.User; import at.tuwien.exception.*; import at.tuwien.mapper.DatabaseMapper; import at.tuwien.mapper.QueryMapper; @@ -15,8 +13,8 @@ import at.tuwien.mapper.TableMapper; import at.tuwien.repository.mdb.DatabaseRepository; import at.tuwien.repository.sdb.DatabaseIdxRepository; import at.tuwien.service.DatabaseService; -import at.tuwien.service.SemanticService; import at.tuwien.service.TableService; +import at.tuwien.service.UserService; import at.tuwien.utils.UserUtil; import com.mchange.v2.c3p0.ComboPooledDataSource; import lombok.extern.log4j.Log4j2; @@ -29,6 +27,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -38,17 +37,19 @@ public class TableServiceImpl extends HibernateConnector implements TableService private final QueryMapper queryMapper; private final TableMapper tableMapper; + private final UserService userService; private final DatabaseMapper databaseMapper; private final DatabaseService databaseService; private final DatabaseRepository databaseRepository; private final DatabaseIdxRepository databaseIdxRepository; @Autowired - public TableServiceImpl(QueryMapper queryMapper, TableMapper tableMapper, DatabaseMapper databaseMapper, - DatabaseService databaseService, DatabaseRepository databaseRepository, - DatabaseIdxRepository databaseIdxRepository) { + public TableServiceImpl(QueryMapper queryMapper, TableMapper tableMapper, UserService userService, + DatabaseMapper databaseMapper, DatabaseService databaseService, + DatabaseRepository databaseRepository, DatabaseIdxRepository databaseIdxRepository) { this.queryMapper = queryMapper; this.tableMapper = tableMapper; + this.userService = userService; this.databaseMapper = databaseMapper; this.databaseService = databaseService; this.databaseRepository = databaseRepository; @@ -131,7 +132,7 @@ public class TableServiceImpl extends HibernateConnector implements TableService @Transactional public Table createTable(Long databaseId, TableCreateDto createDto, Principal principal) throws ImageNotSupportedException, DatabaseNotFoundException, TableMalformedException, - TableNameExistsException, QueryMalformedException, TableNotFoundException { + TableNameExistsException, QueryMalformedException, TableNotFoundException, UserNotFoundException { /* find */ final Database database = databaseService.find(databaseId); if (!database.getContainer().getImage().getName().equals("mariadb")) { @@ -148,6 +149,7 @@ public class TableServiceImpl extends HibernateConnector implements TableService throw new TableNameExistsException("Failed to create table with name " + internalName + ": exists in metadata database"); } final Table table = tableMapper.tableCreateDtoToTable(createDto); + final User owner = userService.find(UserUtil.getId(principal)); /* run query */ final ComboPooledDataSource dataSource = getPrivilegedDataSource(database.getContainer().getImage(), database.getContainer(), database); final Boolean generatedSequence; @@ -163,9 +165,11 @@ public class TableServiceImpl extends HibernateConnector implements TableService table.setIsVersioned(true); table.setTdbid(databaseId); table.setDatabase(database); - table.setConstraints(null); + table.setCreator(owner); table.setCreatedBy(UserUtil.getId(principal)); + table.setOwner(owner); table.setOwnedBy(UserUtil.getId(principal)); + table.setIdentifiers(new LinkedList<>()); /* map columns */ table.setColumns(createDto.getColumns() .stream() diff --git a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java index 027e8c8b1f..b8e43cdd23 100644 --- a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java +++ b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java @@ -1037,15 +1037,6 @@ public abstract class BaseTest { public final static UUID DATABASE_2_OWNER = USER_2_ID; public final static UUID DATABASE_2_CREATOR = USER_2_ID; - public final static DatabaseBriefDto DATABASE_2_DTO_BRIEF = DatabaseBriefDto.builder() - .id(DATABASE_2_ID) - .container(CONTAINER_2_DTO_BRIEF) - .created(DATABASE_2_CREATED) - .isPublic(DATABASE_2_PUBLIC) - .name(DATABASE_2_NAME) - .internalName(DATABASE_2_INTERNALNAME) - .build(); - public final static DatabaseCreateDto DATABASE_2_CREATE = DatabaseCreateDto.builder() .name(DATABASE_2_NAME) .isPublic(DATABASE_2_PUBLIC) @@ -1074,14 +1065,6 @@ public abstract class BaseTest { .views(List.of()) .build(); - public final static DatabaseBriefDto DATABASE_3_DTO_BRIEF = DatabaseBriefDto.builder() - .id(DATABASE_3_ID) - .created(DATABASE_3_CREATED) - .isPublic(DATABASE_3_PUBLIC) - .name(DATABASE_3_NAME) - .internalName(DATABASE_3_INTERNALNAME) - .build(); - public final static DatabaseCreateDto DATABASE_3_CREATE = DatabaseCreateDto.builder() .name(DATABASE_3_NAME) .isPublic(DATABASE_3_PUBLIC) @@ -1114,18 +1097,6 @@ public abstract class BaseTest { .views(List.of()) .build(); - public final static DatabaseBriefDto DATABASE_4_DTO_BRIEF = DatabaseBriefDto.builder() - .id(DATABASE_4_ID) - .created(Instant.now().minus(4, HOURS)) - .isPublic(DATABASE_4_PUBLIC) - .name(DATABASE_4_NAME) - .description(DATABASE_4_DESCRIPTION) - .internalName(DATABASE_4_INTERNALNAME) - .created(DATABASE_4_CREATED) - .creator(USER_4_BRIEF_DTO) - .owner(USER_4_BRIEF_DTO) - .build(); - public final static TableCreateDto TABLE_0_CREATE_DTO = TableCreateDto.builder() .name("full") .description("full example") @@ -2803,7 +2774,6 @@ public abstract class BaseTest { public final static QueryResultDto QUERY_4_RESULT_DTO = QueryResultDto.builder() .id(QUERY_4_RESULT_ID) - .resultNumber(QUERY_4_RESULT_NUMBER) .result(QUERY_4_RESULT_RESULT) .build(); @@ -4137,6 +4107,8 @@ public abstract class BaseTest { public final static ConstraintsDto TABLE_3_CONSTRAINTS_DTO = ConstraintsDto.builder() .uniques(List.of(UniqueDto.builder().columns(List.of(TABLE_3_COLUMNS_DTO.get(0))).build())) + .foreignKeys(List.of()) + .checks(Set.of()) .build(); public final static List<TableColumn> TABLE_5_COLUMNS = List.of(TableColumn.builder() @@ -4835,7 +4807,7 @@ public abstract class BaseTest { .referencedColumns(List.of(COLUMN_4_1_NAME)) .build()); - public final static List<String> TABLE_6_CHECKS_CREATE = List.of( + public final static Set<String> TABLE_6_CHECKS_CREATE = Set.of( COLUMN_5_2_NAME + " != " + COLUMN_5_3_NAME); public final static ConstraintsCreateDto TABLE_6_CONSTRAINTS_CREATE = ConstraintsCreateDto.builder() @@ -5376,7 +5348,6 @@ public abstract class BaseTest { public final static QueryResultDto QUERY_1_RESULT_DTO = QueryResultDto.builder() .id(QUERY_1_RESULT_ID) - .resultNumber(QUERY_1_RESULT_NUMBER) .result(QUERY_1_RESULT_RESULT) .build(); @@ -6811,15 +6782,6 @@ public abstract class BaseTest { .views(List.of(VIEW_1_DTO, VIEW_2_DTO, VIEW_3_DTO)) .build(); - public final static DatabaseBriefDto DATABASE_1_DTO_BRIEF = DatabaseBriefDto.builder() - .id(DATABASE_1_ID) - .container(CONTAINER_1_DTO_BRIEF) - .created(Instant.now().minus(1, HOURS)) - .isPublic(DATABASE_1_PUBLIC) - .name(DATABASE_1_NAME) - .internalName(DATABASE_1_INTERNALNAME) - .build(); - public final static DatabaseAccess DATABASE_1_USER_1_READ_ACCESS = DatabaseAccess.builder() .type(AccessType.READ) .hdbid(DATABASE_1_ID) diff --git a/dbrepo-ui/bun.lockb b/dbrepo-ui/bun.lockb index 8493ca2eeeac087f85096ca4acb9a406944112bf..90e197bf8ac360877c05d20dec7b22b6a8c537ec 100755 GIT binary patch delta 33 pcmdlnLu|(kv4$4L7N#xC`kUDq<4pC8^$gn0HZyNG+swjr69C7t3q1e; delta 33 lcmdlnLu|(kv4$4L7N#xC`kUFA7{H+2Y%}wAv&}3_HvzSD3G4s> diff --git a/dbrepo-ui/components/dialogs/TimeTravel.vue b/dbrepo-ui/components/dialogs/TimeTravel.vue index 8637cd456e..ed1bcc527e 100644 --- a/dbrepo-ui/components/dialogs/TimeTravel.vue +++ b/dbrepo-ui/components/dialogs/TimeTravel.vue @@ -136,7 +136,7 @@ export default { try { this.loading = true const tableService = useTableService() - this.history = await tableService.history(this.table.tdbid, this.table.id) + this.history = await tableService.history(this.table.database_id, this.table.id) // this.chartData.labels = history.map(d => format(new Date(d.timestamp), 'dd.MM.yyyy HH:mm:ss')) // this.chartData.datasets = [{ // // backgroundColor: 'red', diff --git a/dbrepo-ui/components/subset/Results.vue b/dbrepo-ui/components/subset/Results.vue index 503366ab58..8881c334c9 100644 --- a/dbrepo-ui/components/subset/Results.vue +++ b/dbrepo-ui/components/subset/Results.vue @@ -42,7 +42,7 @@ export default { showFirstLastPage: true, itemsPerPageOptions: [10, 25, 50, 100] }, - total: -1 + total: null } }, computed: { @@ -150,9 +150,6 @@ export default { }) console.debug('query result', data) this.result.rows = data.result - if (this.total < 0 && data.result_number != null) { - this.total = data.result_number - } } } } diff --git a/dbrepo-ui/components/table/TableImport.vue b/dbrepo-ui/components/table/TableImport.vue index 6af8769d2f..d63ae82ff3 100644 --- a/dbrepo-ui/components/table/TableImport.vue +++ b/dbrepo-ui/components/table/TableImport.vue @@ -458,7 +458,8 @@ export default { this.$toast.success(this.$t('success.analyse.dataset')) this.$emit('analyse', {columns: this.columns, filename, line_termination}) }) - .catch(() => { + .catch((error) => { + console.error('Failed to analyse dataset', error) this.loading = false }) .finally(() => { diff --git a/dbrepo-ui/composables/analyse-service.ts b/dbrepo-ui/composables/analyse-service.ts index 6968311a72..83e1069cae 100644 --- a/dbrepo-ui/composables/analyse-service.ts +++ b/dbrepo-ui/composables/analyse-service.ts @@ -3,7 +3,7 @@ export const useAnalyseService = (): any => { const axios = useAxiosInstance() console.debug('suggest data types for columns') return new Promise<DataTypesDto[]>((resolve, reject) => { - axios.post<DataTypesDto[]>('/api/analyse/determinedt', data) + axios.get<DataTypesDto[]>('/api/analyse/datatypes', { params: data }) .then((response) => { console.info('Suggested data types for column(s)') resolve(response.data) diff --git a/dbrepo-ui/composables/axios-instance.ts b/dbrepo-ui/composables/axios-instance.ts index e649bbacf2..274b4792cc 100644 --- a/dbrepo-ui/composables/axios-instance.ts +++ b/dbrepo-ui/composables/axios-instance.ts @@ -41,7 +41,7 @@ export const useAxiosInstance = () => { config.headers.Authorization = `Bearer ${response.access_token}` return config }) - .error((error: AxiosError) => { + .catch((error: AxiosError) => { if (parseKeycloakError(error)?.error == 'invalid_grant') { console.error('Invalid user credentials: perform logout') userStore.logout() diff --git a/dbrepo-ui/composables/database-service.ts b/dbrepo-ui/composables/database-service.ts index 0e6299502c..d73cc29c4b 100644 --- a/dbrepo-ui/composables/database-service.ts +++ b/dbrepo-ui/composables/database-service.ts @@ -1,9 +1,9 @@ export const useDatabaseService = (): any => { - async function findAll(): Promise<DatabaseBriefDto[]> { + async function findAll(): Promise<DatabaseDto[]> { const axios = useAxiosInstance(); console.debug('find databases'); - return new Promise((resolve, reject) => { - axios.get<DatabaseBriefDto[]>('/api/database') + return new Promise<DatabaseDto[]>((resolve, reject) => { + axios.get<DatabaseDto[]>('/api/database') .then((response) => { console.info(`Found ${response.data.length} database(s)`); resolve(response.data); @@ -15,6 +15,23 @@ export const useDatabaseService = (): any => { }); } + async function findCount(): Promise<number> { + const axios = useAxiosInstance(); + console.debug('find databases count'); + return new Promise<number>((resolve, reject) => { + axios.head<number>('/api/database') + .then((response) => { + const count: number = Number(response.headers['x-count']) + console.info(`Found ${count} database(s)`); + resolve(count); + }) + .catch((error) => { + console.error('Failed to find databases', error); + reject(error); + }); + }); + } + async function findOne(id: number): Promise<DatabaseDto | null> { const axios = useAxiosInstance(); console.debug('find databases with id', id); @@ -95,7 +112,7 @@ export const useDatabaseService = (): any => { }) } - function databaseToOwner (database: DatabaseDto) { + function databaseToOwner(database: DatabaseDto) { if (!database) { return null } @@ -103,7 +120,7 @@ export const useDatabaseService = (): any => { return userService.userToFullName(database.owner) } - function databaseToContact (database: DatabaseDto) { + function databaseToContact(database: DatabaseDto) { if (!database) { return null } @@ -111,7 +128,7 @@ export const useDatabaseService = (): any => { return userService.userToFullName(database.contact) } - function databaseToJsonLd (database: DatabaseDto): Dataset { + function databaseToJsonLd(database: DatabaseDto): Dataset { const jsonLd: Dataset = { '@context': 'https://schema.org/', '@type': 'Dataset', @@ -140,12 +157,23 @@ export const useDatabaseService = (): any => { return jsonLd } - function isOwner (database: DatabaseDto, user: UserDto): boolean { + function isOwner(database: DatabaseDto, user: UserDto): boolean { if (!database || !user) { return false } return database.owner.id === user.id } - return {findAll, findOne, updateVisibility, updateImage, updateOwner, create, databaseToOwner, databaseToContact, databaseToJsonLd, isOwner} + return { + findAll, + findOne, + updateVisibility, + updateImage, + updateOwner, + create, + databaseToOwner, + databaseToContact, + databaseToJsonLd, + isOwner + } } diff --git a/dbrepo-ui/composables/query-service.ts b/dbrepo-ui/composables/query-service.ts index 0c3c058b9a..6ba3919428 100644 --- a/dbrepo-ui/composables/query-service.ts +++ b/dbrepo-ui/composables/query-service.ts @@ -104,14 +104,14 @@ export const useQueryService = (): any => { }) } - async function reExecuteCount(databaseId: number, queryId: number): Promise<QueryResultDto> { + async function reExecuteCount(databaseId: number, queryId: number): Promise<number> { const axios = useAxiosInstance() console.debug('re-execute query in database with id', databaseId) - return new Promise<QueryResultDto>((resolve, reject) => { - axios.get<QueryResultDto>(`/api/database/${databaseId}/query/${queryId}/data/count`) + return new Promise<number>((resolve, reject) => { + axios.head<void>(`/api/database/${databaseId}/query/${queryId}/data`) .then((response) => { console.info('Re-executed query in database with id', databaseId) - resolve(response.data) + resolve(Number(response.headers['X-Count'])) }) .catch((error) => { console.error('Failed to re-execute query', error) diff --git a/dbrepo-ui/composables/table-service.ts b/dbrepo-ui/composables/table-service.ts index b351802401..418c1ad04f 100644 --- a/dbrepo-ui/composables/table-service.ts +++ b/dbrepo-ui/composables/table-service.ts @@ -86,7 +86,7 @@ export const useTableService = (): any => { const axios = useAxiosInstance() console.debug('get data count for table with id', tableId, 'in database with id', databaseId); return new Promise<number>((resolve, reject) => { - axios.get<number>(`/api/database/${databaseId}/table/${tableId}/data/count`, {params: mapFilter(timestamp, null, null)}) + axios.head<number>(`/api/database/${databaseId}/table/${tableId}/data`, {params: mapFilter(timestamp, null, null)}) .then((response) => { console.info('Got data count for table with id', tableId, 'in database with id', databaseId) resolve(response.data) diff --git a/dbrepo-ui/composables/upload-service.ts b/dbrepo-ui/composables/upload-service.ts index 429f27180e..63db245c2d 100644 --- a/dbrepo-ui/composables/upload-service.ts +++ b/dbrepo-ui/composables/upload-service.ts @@ -35,7 +35,9 @@ export const useUploadService = (): any => { console.error('Failed to match file name', matches) reject(new Error('Failed to match file name')) } else { - resolve(matches[0].replace('files/', '')) + const filename = matches[0].replace('files/', '') + console.debug('Filename cropped as', filename) + resolve(filename) } } } diff --git a/dbrepo-ui/composables/view-service.ts b/dbrepo-ui/composables/view-service.ts index a0ede302ce..31ee8ba226 100644 --- a/dbrepo-ui/composables/view-service.ts +++ b/dbrepo-ui/composables/view-service.ts @@ -47,14 +47,15 @@ export const useViewService = (): any => { }) } - async function reExecuteCount(databaseId: number, viewId: number): Promise<QueryResultDto> { + async function reExecuteCount(databaseId: number, viewId: number): Promise<number> { const axios = useAxiosInstance() console.debug('re-execute view with id', viewId, 'in database with id', databaseId) - return new Promise<QueryResultDto>((resolve, reject) => { - axios.get<QueryResultDto>(`/api/database/${databaseId}/view/${viewId}/data/count`) + return new Promise<number>((resolve, reject) => { + axios.head<number>(`/api/database/${databaseId}/view/${viewId}/data`) .then((response) => { + const count: number = Number(response.headers['X-Count']) console.info('Re-executed view with id', viewId, 'in database with id', databaseId) - resolve(response.data) + resolve(count) }) .catch((error) => { console.error('Failed to re-execute view', error) diff --git a/dbrepo-ui/dto/index.ts b/dbrepo-ui/dto/index.ts index 366ef87a47..9cafc22971 100644 --- a/dbrepo-ui/dto/index.ts +++ b/dbrepo-ui/dto/index.ts @@ -1,42 +1,23 @@ -interface DatabaseBriefDto { - id: number; - name: string; - internal_name: string; - is_public: boolean; - exchange_name: string; - exchange_type: string; - description: string; - creator: UserBriefDto; - owner: UserBriefDto; - created: Date; - container: ContainerBriefDto; - image: any; - accesses: any[]; - identifier: any[]; - tables: TableBriefDto[]; - views: any[]; -} - interface DatabaseDto { id: number; name: string; - internal_name: string; - is_public: boolean; - exchange_name: string; - exchange_type: string; - description: string; creator: UserDto; owner: UserDto; contact: UserDto; created: Date; - identifiers: IdentifierDto[]; - subsets: IdentifierDto[]; + exchange_name: string; + internal_name: string; + is_public: boolean; + description: string | null; container: ContainerBriefDto; - image: ImageDto; + identifiers: IdentifierDto[] | []; + subsets: IdentifierDto[] | []; + image: string; accesses: DatabaseAccessDto[]; identifier: IdentifierDto[]; tables: TableDto[]; views: ViewDto[]; + exchange_type: string | null; } interface DatabaseCreateDto { @@ -65,15 +46,15 @@ interface UserDto { id: string; username: string; attributes: UserAttributesDto; - name: string; - qualified_name: string; - given_name: string; - family_name: string; + name: string | null; + qualified_name: string | null; + given_name: string | null; + family_name: string | null; } interface UserAttributesDto { - orcid: string; - affiliation: string; + orcid: string | null; + affiliation: string | null; theme: string; } @@ -130,7 +111,7 @@ interface ColumnBriefDto { interface TableDto { id: number; - tdbid: number; + database_id: number; name: string; identifiers: IdentifierDto[]; creator: UserDto; @@ -152,10 +133,19 @@ interface TableDto { avg_row_length: number; } +interface ForeignKeyDto { + name: string; + columns: ColumnDto[]; + referenced_table: TableDto; + referenced_columns: ColumnDto[]; + on_update: string | null; + on_delete: string | null; +} + interface ConstraintsDto { uniques: UniqueDto[]; - checks: any; - foreign_keys: any; + checks: string[]; + foreign_keys: ForeignKeyDto[]; } interface DetermineDataTypesDto { @@ -180,18 +170,18 @@ interface UniqueDto { interface IdentifierSaveDto { type: string; titles: IdentifierSaveTitleDto[]; - descriptions: IdentifierSaveDescriptionDto[]; - funders: IdentifierFunderSaveDto[]; - licenses: LicenseDto[]; + descriptions: IdentifierSaveDescriptionDto[] | []; + funders: IdentifierFunderSaveDto[] | []; + licenses: LicenseDto[] | []; publisher: string; - language: string; + language: string | null; creators: CreatorSaveDto[]; - database_id: number; - query_id: number; - view_id: number; - table_id: number; - publication_day: number; - publication_month: number; + database_id: number | null; + query_id: number | null; + view_id: number | null; + table_id: number | null; + publication_day: number | null; + publication_month: number | null; publication_year: number; related_identifiers: RelatedIdentifierSaveDto[]; } @@ -220,31 +210,31 @@ interface IdentifierFunderSaveDto { interface IdentifierDto { id: number; type: string; - titles: IdentifierTitleDto[]; - descriptions: IdentifierDescriptionDto[]; - funders: IdentifierFunderDto[]; - query: string; - execution: Date; - doi: string; - publisher: string; - language: string; - licenses: LicenseDto[]; - creators: CreatorDto[]; + titles: IdentifierTitleDto[] | []; + descriptions: IdentifierDescriptionDto[] | []; + funders: IdentifierFunderDto[] | []; + query: string | null; + execution: Date | null; + doi: string | null; + publisher: string | null; + language: string | null; + licenses: LicenseDto[] | []; + creators: CreatorDto[] | []; created: Date; - database_id: number; - query_id: number; - table_id: number; - view_id: number; - query_normalized: string; - related_identifiers: RelatedIdentifierDto[]; - query_hash: string; - result_hash: string; + database_id: number | null; + query_id: number | null; + table_id: number | null; + view_id: number | null; + query_normalized: string | null; + related_identifiers: RelatedIdentifierDto[] | []; + query_hash: string | null; + result_hash: string | null; /** * @deprecated */ - result_number: number; - publication_day: number; - publication_month: number; + result_number: number | null; + publication_day: number | null; + publication_month: number | null; publication_year: number; last_modified: Date; } @@ -468,7 +458,7 @@ interface KeycloakErrorDto { interface ViewBriefDto { id: number; - vdbid: number; + database_id: number; name: string; identifier: any[]; query: string; @@ -541,13 +531,9 @@ interface ImportCsv { } interface QueryResultDto { + id: number | null; result: any; headers: any; - id: number; - /** - * @deprecated Will be removed with v2 - */ - result_number: number | null; } interface TableHistoryDto { diff --git a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/info.vue index 56c997649e..c965fbbb04 100644 --- a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/info.vue @@ -42,7 +42,7 @@ </v-list-item> <v-list-item :title="$t('pages.view.visibility.title')"> - {{ view.is_public ? $t('layout.public') : $t('layout.private') }} + {{ view.is_public ? $t('toolbars.database.public') : $t('toolbars.database.private') }} </v-list-item> </v-list> </v-card-text> diff --git a/dbrepo-ui/pages/search.vue b/dbrepo-ui/pages/search.vue index 491c366e85..8b132ee89f 100644 --- a/dbrepo-ui/pages/search.vue +++ b/dbrepo-ui/pages/search.vue @@ -3,7 +3,12 @@ <v-toolbar variant="flat"> <v-toolbar-title> - <span v-if="header" v-text="header" /> + <span + v-if="header" + v-text="header" /> + <v-skeleton-loader + v-if="!header" + type="heading" /> </v-toolbar-title> <v-spacer /> <v-btn diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 89aeb123ed..159535ff95 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -141,6 +141,7 @@ services: KEYCLOAK_HOST: "${KEYCLOAK_HOST:-http://authentication-service:8080}" KEYCLOAK_ADMIN: "${KEYCLOAK_ADMIN:-fda}" KEYCLOAK_ADMIN_PASSWORD: "${KEYCLOAK_ADMIN_PASSWORD:-fda}" + KEYCLOAK_CLIENT_SECRET: "${KEYCLOAK_CLIENT_SECRET:-MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG}" DATACITE_URL: "${DATACITE_URL:-https://api.test.datacite.org}" DATACITE_PREFIX: "${DATACITE_PREFIX:-}" DATACITE_USERNAME: "${DATACITE_USERNAME:-}" @@ -150,6 +151,7 @@ services: S3_SECRET_ACCESS_KEY: "${STORAGE_PASSWORD:-seaweedfsadmin}" S3_IMPORT_BUCKET: "${STORAGE_IMPORT_BUCKET:-dbrepo-upload}" S3_EXPORT_BUCKET: "${STORAGE_EXPORT_BUCKET:-dbrepo-download}" + DELETE_STALE_FILES_RATE: "${DELETE_STALE_FILES_RATE:-60}" MIRROR_RATE: ${METADATA_SERVICE_MIRROR_RATE:-60} OBTAIN_METADATA_RATE: ${METADATA_SERVICE_OBTAIN_METADATA_RATE:-60} DELETE_STALE_QUERIES_RATE: ${METADATA_SERVICE_DELETE_STALE_QUERIES_RATE:-60} @@ -195,7 +197,7 @@ services: restart: "no" container_name: dbrepo-broker-service hostname: broker-service - image: docker.io/bitnami/rabbitmq:3.10 + image: docker.io/bitnami/rabbitmq:3.12-debian-12 ports: - "5672:5672" - "15672:15672" diff --git a/docker-compose.yml b/docker-compose.yml index 1bb2a6d46c..26679c292d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -209,7 +209,7 @@ services: restart: "no" container_name: dbrepo-broker-service hostname: broker-service - image: docker.io/bitnami/rabbitmq:3.10 + image: docker.io/bitnami/rabbitmq:3.12-debian-12 ports: - "5672:5672" - "15672:15672" @@ -413,9 +413,7 @@ services: restart: "no" container_name: dbrepo-upload-service hostname: upload-service - image: docker.io/tusproject/tusd:v1.12 - ports: - - "1080:1080" + image: docker.io/tusproject/tusd:v2.4.0 command: - "--base-path=/api/upload/files/" - "-s3-endpoint=${STORAGE_ENDPOINT:-http://storage-service:9000}" @@ -428,7 +426,7 @@ services: dbrepo-storage-service: condition: service_healthy healthcheck: - test: wget -qO- localhost:1080/metrics | grep "tusd" || exit 1 + test: wget -qO- localhost:8080/metrics | grep "tusd" || exit 1 interval: 10s timeout: 5s retries: 12 diff --git a/helm-charts/dbrepo/Chart.lock b/helm-charts/dbrepo/Chart.lock index b43e0b5c27..e427532853 100644 --- a/helm-charts/dbrepo/Chart.lock +++ b/helm-charts/dbrepo/Chart.lock @@ -26,5 +26,5 @@ dependencies: - name: seaweedfs repository: https://seaweedfs.github.io/seaweedfs/helm version: 3.59.4 -digest: sha256:408d622f6f8b819434771021ba4144bd3d58aef6b1302f323f0acba672b0e7ec -generated: "2024-01-07T22:24:18.122775309+01:00" +digest: sha256:a8cdc5c9c76c732d2997450dd92af1fe17686cea93d3b521185b6be17e9fa536 +generated: "2024-03-18T07:22:03.360916672+01:00" diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000000..004cb4a881 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1,7 @@ +# IDE +.idea/ + +# environment +.env +.venv/ +venv/ \ No newline at end of file diff --git a/lib/python/.gitignore b/lib/python/.gitignore new file mode 100644 index 0000000000..57f88d47ba --- /dev/null +++ b/lib/python/.gitignore @@ -0,0 +1,20 @@ +# IDE +.idea/ + +# generated +dist/ +dbrepo.egg-info/ +build/ + +# coverage +coverage.txt +.coverage +report.xml + +# environment +.env +.venv/ +venv/ + +# secrets +../.pypirc \ No newline at end of file diff --git a/lib/python/.pypirc b/lib/python/.pypirc new file mode 100644 index 0000000000..e5efbb7349 --- /dev/null +++ b/lib/python/.pypirc @@ -0,0 +1,18 @@ +[distutils] +index-servers = + pypi + testpypi + +[pypi] +repository = https://upload.pypi.org/legacy/ +username = __token__ +password = pypi-AgEIcHlwaS5vcmcCJDMxNGY1MTk3LWJhOGUtNDBkOC04MmI5LTYwZTM2YTQ1YjI4NgACDlsxLFsiZGJyZXBvIl1dAAIsWzIsWyI2NjQ1YWIwNC1jMjE0LTQ0ODYtOTZkMi03ZWU3ZTk1ODVkYmMiXV0AAAYgkpc5OaB--EyobeUNAXW6kuH0b8wcD91kr5ollMF45vU +sign = pgp +identity = 2D40566BEE74C4DF6043309F5B3681E365CC53BC + +[testpypi] +repository = https://test.pypi.org/legacy/ +username = __token__ +password = pypi-AgENdGVzdC5weXBpLm9yZwIkNWNjY2U2OWEtZmQ2YS00OGEzLTgyYzMtNDI1Njg1NTdjYTM1AAIOWzEsWyJkYnJlcG8iXV0AAixbMixbIjkyN2YyMzQ1LWMxMWYtNGYwZi05YzU2LTlmNDY5ZWRmMWE4NCJdXQAABiCR5npHFdD8wc4CRDn5VUQxyRwXxgTMOJTQP3Hujxv4xA +sign = pgp +identity = 2D40566BEE74C4DF6043309F5B3681E365CC53BC \ No newline at end of file diff --git a/lib/python/LICENSE b/lib/python/LICENSE new file mode 100644 index 0000000000..7a4a3ea242 --- /dev/null +++ b/lib/python/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/lib/python/Makefile b/lib/python/Makefile new file mode 100644 index 0000000000..0e9f2e224b --- /dev/null +++ b/lib/python/Makefile @@ -0,0 +1,26 @@ +all: + +clean: + rm -rf ./python/dist/* ./docs/build/* + +install: + pipenv install + +docs: clean + sphinx-apidoc -o ./docs/source ./dbrepo + sphinx-build -M html ./docs/ ./docs/build/ + +check: + python3 ./python/setup.py develop + +build: clean + python3 -m build --sdist . + python3 -m build --wheel . + +deploy: build + python3 -m twine upload --config-file ./.pypirc --verbose --repository pypi ./dist/dbrepo-* + +deploy-test: build + python3 -m twine upload --config-file ./.pypirc --verbose --repository testpypi ./dist/dbrepo-* + +FORCE: ; \ No newline at end of file diff --git a/lib/python/Pipfile b/lib/python/Pipfile new file mode 100644 index 0000000000..486a3470b3 --- /dev/null +++ b/lib/python/Pipfile @@ -0,0 +1,23 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pika = "*" +requests = "~=2.31" +dataclasses = "*" +dataclasses-json = "*" +tuspy = "*" + +[dev-packages] +build = "~=1.1" +setuptools = "*" +twine = "*" +coverage = "*" +pytest = "*" +requests-mock = "*" +furo = "*" + +[requires] +python_version = "3.11" diff --git a/lib/python/Pipfile.lock b/lib/python/Pipfile.lock new file mode 100644 index 0000000000..983aa9e223 --- /dev/null +++ b/lib/python/Pipfile.lock @@ -0,0 +1,1340 @@ +{ + "_meta": { + "hash": { + "sha256": "88bda8c0b16c994bfa4e4b7254c7b4fbabb7a5621549bc106489fd124ba0b373" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiohttp": { + "hashes": [ + "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168", + "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb", + "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5", + "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f", + "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc", + "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c", + "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29", + "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4", + "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc", + "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc", + "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63", + "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e", + "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d", + "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a", + "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60", + "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38", + "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b", + "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2", + "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53", + "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5", + "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4", + "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96", + "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58", + "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa", + "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321", + "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae", + "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce", + "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8", + "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194", + "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c", + "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf", + "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d", + "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869", + "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b", + "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52", + "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528", + "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5", + "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1", + "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4", + "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8", + "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d", + "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7", + "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5", + "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54", + "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3", + "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5", + "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c", + "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29", + "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3", + "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747", + "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672", + "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5", + "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11", + "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca", + "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768", + "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6", + "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2", + "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533", + "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6", + "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266", + "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d", + "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec", + "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5", + "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1", + "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b", + "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679", + "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283", + "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb", + "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b", + "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3", + "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051", + "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511", + "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e", + "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d", + "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542", + "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f" + ], + "markers": "python_version >= '3.8'", + "version": "==3.9.3" + }, + "aiosignal": { + "hashes": [ + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "certifi": { + "hashes": [ + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.2.2" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "dataclasses": { + "hashes": [ + "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", + "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" + ], + "index": "pypi", + "version": "==0.6" + }, + "dataclasses-json": { + "hashes": [ + "sha256:73696ebf24936560cca79a2430cbc4f3dd23ac7bf46ed17f38e5e5e7657a6377", + "sha256:f90578b8a3177f7552f4e1a6e535e84293cd5da421fcce0642d49c0d7bdf8df2" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==0.6.4" + }, + "frozenlist": { + "hashes": [ + "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", + "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", + "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", + "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", + "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", + "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", + "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", + "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", + "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", + "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", + "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", + "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", + "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", + "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", + "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", + "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", + "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", + "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", + "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", + "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", + "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", + "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", + "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", + "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", + "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", + "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", + "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", + "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", + "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", + "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", + "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", + "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", + "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", + "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", + "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", + "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", + "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", + "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", + "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", + "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", + "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", + "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", + "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", + "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", + "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", + "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", + "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", + "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", + "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", + "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", + "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", + "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", + "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", + "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", + "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", + "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", + "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", + "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", + "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", + "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", + "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", + "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", + "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", + "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", + "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", + "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", + "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", + "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", + "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", + "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", + "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", + "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", + "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", + "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", + "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", + "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", + "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.1" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "marshmallow": { + "hashes": [ + "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3", + "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633" + ], + "markers": "python_version >= '3.8'", + "version": "==3.21.1" + }, + "multidict": { + "hashes": [ + "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", + "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", + "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", + "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", + "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", + "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", + "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", + "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", + "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", + "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", + "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", + "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", + "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", + "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", + "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", + "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", + "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", + "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", + "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", + "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", + "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", + "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", + "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", + "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", + "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", + "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", + "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", + "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", + "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", + "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", + "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", + "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", + "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", + "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", + "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", + "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", + "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", + "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", + "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", + "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", + "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", + "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", + "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", + "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", + "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", + "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", + "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", + "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", + "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", + "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", + "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", + "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", + "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", + "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", + "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", + "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", + "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", + "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", + "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", + "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", + "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", + "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", + "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", + "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", + "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", + "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", + "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", + "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", + "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", + "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", + "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", + "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", + "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", + "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", + "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", + "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", + "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", + "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", + "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", + "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", + "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", + "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", + "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", + "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", + "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", + "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", + "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", + "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", + "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", + "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.5" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pika": { + "hashes": [ + "sha256:0779a7c1fafd805672796085560d290213a465e4f6f76a6fb19e378d8041a14f", + "sha256:b2a327ddddf8570b4965b3576ac77091b850262d34ce8c1d8cb4e4146aa4145f" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.3.2" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "tinydb": { + "hashes": [ + "sha256:30c06d12383d7c332e404ca6a6103fb2b32cbf25712689648c39d9a6bd34bd3d", + "sha256:6dd686a9c5a75dfa9280088fd79a419aefe19cd7f4bd85eba203540ef856d564" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==4.8.0" + }, + "tuspy": { + "hashes": [ + "sha256:003d24ee1a310266df507bbff9859120098c026abb5e7b77141292003b0aca12", + "sha256:024d3d1745120098a85635e42242039ca6b1bc787f561ec974fffb45fc775c1b" + ], + "index": "pypi", + "markers": "python_full_version >= '3.5.3'", + "version": "==1.0.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + ], + "markers": "python_version >= '3.8'", + "version": "==4.10.0" + }, + "typing-inspect": { + "hashes": [ + "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", + "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" + ], + "version": "==0.9.0" + }, + "urllib3": { + "hashes": [ + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.1" + }, + "yarl": { + "hashes": [ + "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", + "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", + "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", + "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", + "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", + "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", + "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", + "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", + "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", + "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", + "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", + "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", + "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", + "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", + "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", + "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", + "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", + "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", + "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", + "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", + "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", + "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", + "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", + "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", + "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", + "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", + "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", + "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", + "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", + "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", + "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", + "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", + "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", + "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", + "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", + "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", + "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", + "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", + "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", + "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", + "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", + "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", + "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", + "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", + "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", + "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", + "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", + "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", + "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", + "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", + "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", + "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", + "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", + "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", + "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", + "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", + "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", + "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", + "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", + "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", + "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", + "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", + "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", + "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", + "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", + "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", + "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", + "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", + "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", + "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", + "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", + "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", + "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", + "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", + "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", + "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", + "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", + "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", + "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", + "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", + "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", + "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", + "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", + "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", + "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", + "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", + "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", + "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", + "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", + "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.4" + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", + "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92" + ], + "markers": "python_version >= '3.9'", + "version": "==0.7.16" + }, + "babel": { + "hashes": [ + "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", + "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287" + ], + "markers": "python_version >= '3.7'", + "version": "==2.14.0" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", + "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==4.12.3" + }, + "build": { + "hashes": [ + "sha256:8ed0851ee76e6e38adce47e4bee3b51c771d86c64cf578d0c2245567ee200e73", + "sha256:8eea65bb45b1aac2e734ba2cc8dad3a6d97d97901a395bd0ed3e7b46953d2a31" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.1.1" + }, + "certifi": { + "hashes": [ + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.2.2" + }, + "cffi": { + "hashes": [ + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.16.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "coverage": { + "hashes": [ + "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", + "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", + "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", + "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", + "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", + "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", + "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", + "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", + "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", + "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", + "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", + "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", + "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", + "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", + "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", + "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", + "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", + "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", + "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", + "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", + "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", + "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", + "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", + "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", + "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", + "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", + "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", + "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", + "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", + "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", + "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", + "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", + "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", + "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", + "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", + "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", + "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", + "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", + "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", + "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", + "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", + "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", + "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", + "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", + "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", + "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", + "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", + "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", + "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", + "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", + "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", + "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==7.4.4" + }, + "cryptography": { + "hashes": [ + "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", + "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", + "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", + "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", + "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", + "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", + "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", + "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", + "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", + "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", + "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", + "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", + "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", + "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", + "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", + "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", + "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", + "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", + "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", + "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", + "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", + "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", + "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", + "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", + "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", + "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", + "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", + "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", + "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", + "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", + "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", + "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" + ], + "markers": "python_version >= '3.7'", + "version": "==42.0.5" + }, + "docutils": { + "hashes": [ + "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", + "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" + ], + "markers": "python_version >= '3.7'", + "version": "==0.20.1" + }, + "furo": { + "hashes": [ + "sha256:3548be2cef45a32f8cdc0272d415fcb3e5fa6a0eb4ddfe21df3ecf1fe45a13cf", + "sha256:4d6b2fe3f10a6e36eb9cc24c1e7beb38d7a23fc7b3c382867503b7fcac8a1e02" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2024.1.29" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "imagesize": { + "hashes": [ + "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", + "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.4.1" + }, + "importlib-metadata": { + "hashes": [ + "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2" + ], + "markers": "python_version >= '3.8'", + "version": "==7.1.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "jaraco.classes": { + "hashes": [ + "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206", + "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30" + ], + "markers": "python_version >= '3.8'", + "version": "==3.3.1" + }, + "jaraco.context": { + "hashes": [ + "sha256:4dad2404540b936a20acedec53355bdaea223acb88fd329fa6de9261c941566e", + "sha256:5d9e95ca0faa78943ed66f6bc658dd637430f16125d86988e77844c741ff2f11" + ], + "markers": "python_version >= '3.7'", + "version": "==4.3.0" + }, + "jaraco.functools": { + "hashes": [ + "sha256:c279cb24c93d694ef7270f970d499cab4d3813f4e08273f95398651a634f0925", + "sha256:daf276ddf234bea897ef14f43c4e1bf9eefeac7b7a82a4dd69228ac20acff68d" + ], + "markers": "python_version >= '3.8'", + "version": "==4.0.0" + }, + "jeepney": { + "hashes": [ + "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.8.0" + }, + "jinja2": { + "hashes": [ + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.3" + }, + "keyring": { + "hashes": [ + "sha256:9a15cd280338920388e8c1787cb8792b9755dabb3e7c61af5ac1f8cd437cefde", + "sha256:fc024ed53c7ea090e30723e6bd82f58a39dc25d9a6797d866203ecd0ee6306cb" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0.0" + }, + "markdown-it-py": { + "hashes": [ + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, + "markupsafe": { + "hashes": [ + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.5" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "more-itertools": { + "hashes": [ + "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", + "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1" + ], + "markers": "python_version >= '3.8'", + "version": "==10.2.0" + }, + "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" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pkginfo": { + "hashes": [ + "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", + "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097" + ], + "markers": "python_version >= '3.6'", + "version": "==1.10.0" + }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pygments": { + "hashes": [ + "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", + "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + ], + "markers": "python_version >= '3.7'", + "version": "==2.17.2" + }, + "pyproject-hooks": { + "hashes": [ + "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8", + "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.0" + }, + "pytest": { + "hashes": [ + "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", + "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==8.1.1" + }, + "readme-renderer": { + "hashes": [ + "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311", + "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9" + ], + "markers": "python_version >= '3.8'", + "version": "==43.0" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "requests-mock": { + "hashes": [ + "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4", + "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" + }, + "rfc3986": { + "hashes": [ + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "rich": { + "hashes": [ + "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", + "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==13.7.1" + }, + "secretstorage": { + "hashes": [ + "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.3.3" + }, + "setuptools": { + "hashes": [ + "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", + "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c" + ], + "index": "pypi", + "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", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" + ], + "version": "==2.2.0" + }, + "soupsieve": { + "hashes": [ + "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", + "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" + ], + "markers": "python_version >= '3.8'", + "version": "==2.5" + }, + "sphinx": { + "hashes": [ + "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560", + "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5" + ], + "markers": "python_version >= '3.9'", + "version": "==7.2.6" + }, + "sphinx-basic-ng": { + "hashes": [ + "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", + "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.0b2" + }, + "sphinxcontrib-applehelp": { + "hashes": [ + "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619", + "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4" + ], + "markers": "python_version >= '3.9'", + "version": "==1.0.8" + }, + "sphinxcontrib-devhelp": { + "hashes": [ + "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f", + "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3" + ], + "markers": "python_version >= '3.9'", + "version": "==1.0.6" + }, + "sphinxcontrib-htmlhelp": { + "hashes": [ + "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015", + "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04" + ], + "markers": "python_version >= '3.9'", + "version": "==2.0.5" + }, + "sphinxcontrib-jsmath": { + "hashes": [ + "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", + "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.1" + }, + "sphinxcontrib-qthelp": { + "hashes": [ + "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6", + "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182" + ], + "markers": "python_version >= '3.9'", + "version": "==1.0.7" + }, + "sphinxcontrib-serializinghtml": { + "hashes": [ + "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7", + "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f" + ], + "markers": "python_version >= '3.9'", + "version": "==1.1.10" + }, + "twine": { + "hashes": [ + "sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4", + "sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.1" + }, + "zipp": { + "hashes": [ + "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", + "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" + ], + "markers": "python_version >= '3.8'", + "version": "==3.18.1" + } + } +} diff --git a/lib/python/README.md b/lib/python/README.md new file mode 100644 index 0000000000..56ab7d13af --- /dev/null +++ b/lib/python/README.md @@ -0,0 +1,90 @@ +# DBRepo Python Library + +Official client library for [DBRepo](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/__APPVERSION__/), a database +repository to support research. + +## Installing + +```console +$ python -m pip install dbrepo +``` + +This package supports Python 3.11+. + +## Quickstart + +Create a table and import a .csv file from your computer. + +```python +from dbrepo.RestClient import RestClient +from dbrepo.api.dto import CreateTableColumn, ColumnType, CreateTableConstraints + +client = RestClient(endpoint='https://test.dbrepo.tuwien.ac.at', username="foo", + password="bar") + +# analyse csv +analysis = client.analyse_datatypes(file_path="sensor.csv", separator=",") +print(f"Analysis result: {analysis}") +# -> columns=(date=date, precipitation=decimal, lat=decimal, lng=decimal), separator=, +# line_termination=\n + +# 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)]) +print(f"Create table result {table}") +# -> (id=1, internal_name=sensor_data, ...) + +client.import_table_data(database_id=1, table_id=1, file_path="sensor.csv", separator=",", + skip_lines=1, line_encoding="\n") +print(f"Finished.") +``` + +## Supported Features & Best-Practices + +- Manage user + account ([docs](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//usage-overview/#create-user-account)) +- Manage + databases ([docs](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//usage-overview/#create-database)) +- Manage database access & + visibility ([docs](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//usage-overview/#private-database-access)) +- Import + dataset ([docs](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//usage-overview/#private-database-access)) +- Create persistent + identifiers ([docs](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//usage-overview/#assign-database-pid)) +- Execute + queries ([docs](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//usage-overview/#export-subset)) +- Get data from tables/views/subsets + +## Future + +- Searching + +## Contact + +* Prof. [Andreas Rauber](https://tiss.tuwien.ac.at/person/39608.html)<sup>TU Wien</sup> +* DI. [Martin Weise](https://tiss.tuwien.ac.at/person/287722.html)<sup>TU Wien</sup> \ No newline at end of file diff --git a/lib/python/build-website.sh b/lib/python/build-website.sh new file mode 100755 index 0000000000..1178d90892 --- /dev/null +++ b/lib/python/build-website.sh @@ -0,0 +1,8 @@ +#!/bin/bash +python3 -m venv ./lib/python/venv +source ./lib/python/venv/bin/activate +PIPENV_PIPFILE=./lib/python/Pipfile pipenv install --dev +sed -i -e "s/__APPVERSION__/${APP_VERSION}/g" ./lib/python/pyproject.toml ./lib/python/setup.py ./lib/python/README.md ./lib/python/docs/conf.py ./lib/python/docs/index.rst +sphinx-apidoc -o ./lib/python/docs/source ./lib/python/dbrepo +sphinx-build -M html ./lib/python/docs/ ./lib/python/docs/build/ +cp -r ./lib/python/docs/build/html ./site/sphinx \ No newline at end of file diff --git a/lib/python/build.sh b/lib/python/build.sh new file mode 100644 index 0000000000..802067b6be --- /dev/null +++ b/lib/python/build.sh @@ -0,0 +1,8 @@ +#!/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 new file mode 100644 index 0000000000..c9fcdc05ff --- /dev/null +++ b/lib/python/dbrepo/AmqpClient.py @@ -0,0 +1,62 @@ +import dataclasses +import os +import pika +import sys +import logging +import json + +from dbrepo.api.dto import CreateData + + +class AmqpClient: + """ + The AmqpClient class for communicating with the DBRepo AMQP API to import data. All parameters can be set also \ + via environment variables, e.g. set endpoint with DBREPO_ENDPOINT. You can override the constructor parameters \ + with the environment variables. + + :param broker_host: The AMQP API host. Optional. Default: "broker-service" + :param broker_port: The AMQP API port. Optional. Default: 5672 + :param broker_virtual_host: The AMQP API virtual host. Optional. Default: "/" + :param username: The AMQP API username. Optional. + :param password: The AMQP API password. Optional. + """ + broker_host: str = None + broker_port: int = 5672 + broker_virtual_host: str = None + username: str = None + password: str = None + + def __init__(self, + broker_host: str = 'broker-service', + broker_port: int = 5672, + broker_virtual_host: str = '/', + username: str = None, + password: str = None) -> None: + 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') + else: + self.broker_virtual_host = broker_virtual_host + self.username = os.environ.get('DBREPO_USERNAME', username) + self.password = os.environ.get('DBREPO_PASSWORD', password) + + def publish(self, exchange: str, routing_key: str, data=dict) -> None: + """ + Publishes data to a given exchange with the given routing key with a blocking connection. + + :param exchange: The exchange name. + :param routing_key: The routing key. + :param data: The data. + """ + parameters = pika.ConnectionParameters(host=self.broker_host, port=self.broker_port, + virtual_host=self.broker_virtual_host, + credentials=pika.credentials.PlainCredentials(self.username, + self.password)) + connection = pika.BlockingConnection(parameters) + channel = connection.channel() + channel.basic_publish(exchange=exchange, routing_key=routing_key, + body=json.dumps(dataclasses.asdict(CreateData(data=data)))) + connection.close() diff --git a/lib/python/dbrepo/RestClient.py b/lib/python/dbrepo/RestClient.py new file mode 100644 index 0000000000..a46e8f21bf --- /dev/null +++ b/lib/python/dbrepo/RestClient.py @@ -0,0 +1,1435 @@ +import dataclasses +import sys +import os +import logging + +import requests +from tusclient.client import TusClient + +from dbrepo.api.dto import * +from dbrepo.api.exceptions import ResponseCodeError, UsernameExistsError, EmailExistsError, NotExistsError, \ + ForbiddenError, MalformedError, NameExistsError, QueryStoreError, MetadataConsistencyError, ExternalSystemError, \ + AuthenticationError, UploadError + + +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. + + :param endpoint: The REST API endpoint. Optional. Default: "http://gateway-service" + :param username: The REST API username. Optional. + :param password: The REST API password. Optional. + :param secure: When set to false, the requests library will not verify the authenticity of your TLS/SSL + certificates (i.e. when using self-signed certificates). Default: true. + """ + endpoint: str = None + username: str = None + password: str = None + secure: bool = None + + def __init__(self, + endpoint: str = 'http://gateway-service', + username: str = None, + password: str = None, + secure: bool = True) -> None: + logging.getLogger('requests').setLevel(logging.INFO) + logging.getLogger('urllib3').setLevel(logging.INFO) + logging.getLogger('dataclasses_json').setLevel(logging.ERROR) + 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' + else: + self.secure = secure + + def _wrapper(self, method: str, url: str, params: [(str,)] = None, payload=None, headers=None, + force_auth: bool = False, stream: bool = False) -> requests.Response: + if force_auth and (self.username is None or self.password is None): + raise AuthenticationError(f"Failed to perform request: authentication required") + url = f'{self.endpoint}{url}' + logging.debug(f'method: {method}') + logging.debug(f'url: {url}') + if params is not None: + logging.debug(f'params: {params}') + if stream is not None: + logging.debug(f'stream: {stream}') + logging.debug(f'secure: {self.secure}') + if headers is not None: + logging.debug(f'headers: {headers}') + if payload is not None: + logging.debug(f'payload: {payload}') + payload = dataclasses.asdict(payload) + if self.username is not None and self.password is not None: + logging.debug(f'username: {self.username}, password: (hidden)') + return requests.request(method=method, url=url, auth=(self.username, self.password), verify=self.secure, + json=payload, headers=headers, params=params, stream=stream) + return requests.request(method=method, url=url, verify=self.secure, json=payload, headers=headers, + params=params, stream=stream) + + def upload(self, file_path: str) -> str: + """ + Uploads a file located at file_path to the Upload Service. + + :param file_path: The location of the file on the local filesystem. + + :returns: Filename on the S3 backend of the Upload Service, if successful. + """ + my_client = TusClient(url=f'{self.endpoint}/api/upload/files/') + uploader = my_client.uploader(file_path=file_path) + uploader.upload() + filename = uploader.url[uploader.url.rfind('/') + 1:uploader.url.rfind('+')] + if filename is None or len(filename) == 0: + raise UploadError(f'Failed to upload the file to {self.endpoint}') + return filename + + def whoami(self) -> str: + """ + Print the username. + """ + if self.username is not None: + logging.info(f"{self.username}") + return self.username + logging.info(f"No username set!") + return None + + def get_users(self) -> List[User]: + """ + Get all users. + + :returns: List of users, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/user' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return User.schema().load(body, many=True) + raise ResponseCodeError(f'Failed to find users: response code: {response.status_code} is not 200 (OK)') + + def get_user(self, user_id: str) -> User: + """ + Get a user with given user id. + + :returns: The user, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises NotExistsError: If theuser does not exist. + """ + url = f'/api/user/{user_id}' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return User.schema().load(body) + if response.status_code == 404: + raise NotExistsError(f'Failed to find user with id {user_id}') + raise ResponseCodeError( + f'Failed to find user with id {user_id}: response code: {response.status_code} is not 200 (OK)') + + def create_user(self, username: str, password: str, email: str) -> UserBrief: + """ + Creates a new user. + + :param username: The username of the new user. Must be unique. + :param password: The password of the new user. + :param email: The email of the new user. Must be unique. + + :returns: The user, if successful. + + :raises ResponseCodeError: If something went wrong with the creation. + :raises UsernameExistsError: The username exists already. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedefault role was not found. + :raises EmailExistsError: The email exists already. + """ + url = f'/api/user' + response = self._wrapper(method="post", url=url, + payload=CreateUser(username=username, password=password, email=email)) + if response.status_code == 201: + body = response.json() + return UserBrief.schema().load(body) + if response.status_code == 403: + raise ForbiddenError(f'Failed to update user password: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to create user: default role not found') + if response.status_code == 409: + raise UsernameExistsError(f'Failed to create user: user with username exists') + if response.status_code == 417: + raise EmailExistsError(f'Failed to create user: user with e-mail exists') + raise ResponseCodeError( + f'Failed to create user: response code: {response.status_code} is not 201 (CREATED)') + + def update_user(self, user_id: str, firstname: str = None, lastname: str = None, affiliation: str = None, + orcid: str = None) -> User: + """ + Updates a user with given user id. + + :param user_id: The user id of the user that should be updated. + :param firstname: The updated given name. Optional. + :param lastname: The updated family name. Optional. + :param affiliation: The updated affiliation identifier. Optional. + :param orcid: The updated ORCID identifier. Optional. + + :returns: The user, if successful. + + :raises ResponseCodeError: If something went wrong with the update. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If theuser does not exist. + """ + url = f'/api/user/{user_id}' + response = self._wrapper(method="put", url=url, force_auth=True, + payload=UpdateUser(firstname=firstname, lastname=lastname, affiliation=affiliation, + orcid=orcid)) + if response.status_code == 202: + body = response.json() + return User.schema().load(body) + if response.status_code == 400: + raise ResponseCodeError(f'Failed to update user: invalid values') + if response.status_code == 403: + raise ForbiddenError(f'Failed to update user password: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update user: user not found') + if response.status_code == 405: + raise ForbiddenError(f'Failed to update user: foreign user') + raise ResponseCodeError( + f'Failed to update user: response code: {response.status_code} is not 202 (ACCEPTED)') + + def update_user_theme(self, user_id: str, theme: str) -> User: + """ + Updates the theme of a user with given user id. + + :param user_id: The user id of the user that should be updated. + :param theme: The updated user theme name. + + :returns: The user, if successful. + + :raises ResponseCodeError: If something went wrong with the update. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If theuser does not exist. + """ + url = f'/api/user/{user_id}/theme' + response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateUserTheme(theme=theme)) + if response.status_code == 202: + body = response.json() + return User.schema().load(body) + if response.status_code == 400: + raise ResponseCodeError(f'Failed to update user theme: invalid values') + if response.status_code == 403: + raise ForbiddenError(f'Failed to update user password: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update user theme: user not found') + if response.status_code == 405: + raise ResponseCodeError(f'Failed to update user theme: foreign user') + raise ResponseCodeError( + f'Failed to update user theme: response code: {response.status_code} is not 202 (ACCEPTED)') + + def update_user_password(self, user_id: str, password: str) -> User: + """ + Updates the password of a user with given user id. + + :param user_id: The user id of the user that should be updated. + :param password: The updated user password. + + :returns: The user, if successful. + + :raises ResponseCodeError: If something went wrong with the update. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If theuser does not exist. + """ + url = f'/api/user/{user_id}/password' + response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateUserPassword(password=password)) + if response.status_code == 202: + body = response.json() + return User.schema().load(body) + if response.status_code == 400: + raise ResponseCodeError(f'Failed to update user password: invalid values') + if response.status_code == 403: + raise ForbiddenError(f'Failed to update user password: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update user password: not found') + if response.status_code == 405: + raise ForbiddenError(f'Failed to update user password: foreign user') + if response.status_code == 503: + raise ResponseCodeError(f'Failed to update user password: keycloak error') + raise ResponseCodeError( + f'Failed to update user theme: response code: {response.status_code} is not 202 (ACCEPTED)') + + def get_containers(self) -> List[ContainerBrief]: + """ + Get all containers. + + :returns: List of containers, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/container' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return ContainerBrief.schema().load(body, many=True) + raise ResponseCodeError(f'Failed to find containers: response code: {response.status_code} is not 200 (OK)') + + def get_databases(self) -> List[Database]: + """ + Get all databases. + + :returns: List of databases, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/database' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return Database.schema().load(body, many=True) + raise ResponseCodeError(f'Failed to find databases: response code: {response.status_code} is not 200 (OK)') + + def get_databases_count(self) -> int: + """ + Count all databases. + + :returns: Count of databases if successful. + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/database' + response = self._wrapper(method="head", url=url) + if response.status_code == 200: + return int(response.headers.get("x-count")) + raise ResponseCodeError(f'Failed to find databases: response code: {response.status_code} is not 200 (OK)') + + def get_database(self, database_id: int) -> Database: + """ + Get a databases with given id. + + :param database_id: The database id. + + :returns: The database, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/database/{database_id}' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return Database.schema().load(body) + if response.status_code == 404: + raise NotExistsError(f'Failed to find database with id {database_id}') + raise ResponseCodeError( + f'Failed to find database with id {database_id}: response code: {response.status_code} is not 200 (OK)') + + def create_database(self, name: str, container_id: int, is_public: bool) -> Database: + """ + Create a databases in a container with given container id. + + :param name: The name of the database. + :param container_id: The container id. + :param is_public: The visibility of the database. If set to true everything will be visible, otherwise only + the metadata (schema, identifiers) will be visible to the public. + + :returns: The database, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database' + response = self._wrapper(method="post", url=url, force_auth=True, + payload=CreateDatabase(name=name, container_id=container_id, is_public=is_public)) + if response.status_code == 201: + body = response.json() + return Database.schema().load(body) + if response.status_code == 403: + raise ForbiddenError(f'Failed to create database: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to create database: container not found') + raise ResponseCodeError( + f'Failed to create database: response code: {response.status_code} is not 201 (CREATED)') + + def update_database_visibility(self, database_id: int, is_public: bool) -> Database: + """ + Updates the database visibility of a database with given database id. + + :param database_id: The database id. + :param is_public: The visibility of the database. If set to true everything will be visible, otherwise only + the metadata (schema, identifiers) will be visible to the public. + + :returns: The database, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedatabase does not exist. + """ + url = f'/api/database/{database_id}' + 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.schema().load(body) + if response.status_code == 403: + raise ForbiddenError(f'Failed to update database visibility: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update database visibility: not found') + raise ResponseCodeError( + f'Failed to update database visibility: response code: {response.status_code} is not 202 (ACCEPTED)') + + def update_database_owner(self, database_id: int, user_id: str) -> Database: + """ + Updates the database owner of a database with given database id. + + :param database_id: The database id. + :param user_id: The user id of the new owner. + + :returns: The database, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises NotExistsError: If thedatabase does not exist. + """ + url = f'/api/database/{database_id}/owner' + 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.schema().load(body) + if response.status_code == 403: + raise ForbiddenError(f'Failed to update database visibility: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update database visibility: not found') + raise ResponseCodeError( + f'Failed to update database visibility: response code: {response.status_code} is not 202 (ACCEPTED)') + + def create_table(self, database_id: int, name: str, columns: List[CreateTableColumn], + constraints: CreateTableConstraints, description: str = None) -> Table: + """ + Updates the database owner of a database with given database id. + + :param database_id: The database id. + :param name: The name of the created table. + :param constraints: The constraints of the created table. + :param columns: The columns of the created table. + :param description: The description of the created table. Optional. + + :returns: The table, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises NameExistsError: If a table with this name already exists. + :raises ForbiddenError: If the action is not allowed. + :raises MalformedError: If the payload is rejected by the service. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/table' + response = self._wrapper(method="post", url=url, force_auth=True, + payload=CreateTable(name=name, description=description, + columns=columns, constraints=constraints)) + if response.status_code == 201: + body = response.json() + return Table.schema().load(body) + if response.status_code == 400: + raise MalformedError(f'Failed to create table: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to create table: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to create table: container not found') + if response.status_code == 409: + raise NameExistsError(f'Failed to create table: table name exists') + raise ResponseCodeError( + f'Failed to create table: response code: {response.status_code} is not 201 (CREATED)') + + def get_tables(self, database_id: int) -> List[Table]: + """ + Get all tables. + + :param database_id: The database id. + + :returns: List of tables, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/database/{database_id}/table' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return Table.schema().load(body, many=True) + raise ResponseCodeError(f'Failed to find tables: response code: {response.status_code} is not 200 (OK)') + + def get_table(self, database_id: int, table_id: int) -> Table: + """ + Get a table with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + + :returns: List of tables, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/table/{table_id}' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return Table.schema().load(body) + if response.status_code == 403: + raise ForbiddenError(f'Failed to find table: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to find table: not found') + raise ResponseCodeError(f'Failed to find table: response code: {response.status_code} is not 200 (OK)') + + def delete_table(self, database_id: int, table_id: int) -> None: + """ + Delete a table with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/table/{table_id}' + response = self._wrapper(method="delete", url=url, force_auth=True) + if response.status_code == 202: + return + if response.status_code == 403: + raise ForbiddenError(f'Failed to delete table: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to delete table: not found') + raise ResponseCodeError(f'Failed to delete table: response code: {response.status_code} is not 202 (ACCEPTED)') + + def get_views(self, database_id: int) -> List[View]: + """ + Gets views of a database with given database id. + + :param database_id: The database id. + + :returns: The list of views, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/view' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return View.schema().load(body, many=True) + if response.status_code == 403: + raise ForbiddenError(f'Failed to find views: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to find views: not found') + raise ResponseCodeError(f'Failed to find views: response code: {response.status_code} is not 200 (OK)') + + def get_view(self, database_id: int, view_id: int) -> View: + """ + Get a view of a database with given database id and view id. + + :param database_id: The database id. + :param view_id: The view id. + + :returns: The view, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/view/{view_id}' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return View.schema().load(body) + if response.status_code == 403: + raise ForbiddenError(f'Failed to find view: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to find view: not found') + raise ResponseCodeError(f'Failed to find view: response code: {response.status_code} is not 200 (OK)') + + def create_view(self, database_id: int, name: str, query: str, is_public: bool) -> View: + """ + Create a view in a database with given database id. + + :param database_id: The database id. + :param name: The name of the created view. + :param query: The query of the created view. + :param is_public: The visibility of the view. If set to true everything will be visible, otherwise only + the metadata (schema, identifiers) will be visible to the public. + + :returns: The created view, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/view' + response = self._wrapper(method="post", url=url, force_auth=True, + payload=CreateView(name=name, query=query, is_public=is_public)) + if response.status_code == 201: + body = response.json() + return View.schema().load(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: + raise ForbiddenError(f'Failed to create view: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to create view: not found') + raise ResponseCodeError(f'Failed to create view: response code: {response.status_code} is not 201 (CREATED)') + + def delete_view(self, database_id: int, view_id: int) -> None: + """ + Deletes a view in a database with given database id and view id. + + :param database_id: The database id. + :param view_id: The view id. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/view/{view_id}' + response = self._wrapper(method="delete", url=url, force_auth=True) + if response.status_code == 202: + return + if response.status_code == 400 or response.status_code == 423: + raise MalformedError(f'Failed to delete view: service rejected malformed payload') + if response.status_code == 403 or response.status_code == 405: + raise ForbiddenError(f'Failed to delete view: not allowed') + if response.status_code == 404: + 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: + """ + Get data of a view in a database with given database id and view id. + + :param database_id: The database id. + :param view_id: The view id. + :param page: The result pagination number. Optional. Default: 0. + :param size: The result pagination size. Optional. Default: 10. + + :returns: The result of the view query, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If the view does not exist. + """ + url = f'/api/database/{database_id}/view/{view_id}/data' + params = [] + if page is not None and size is not None: + params.append(('page', page)) + params.append(('size', size)) + response = self._wrapper(method="get", url=url, params=params) + if response.status_code == 200: + body = response.json() + return Result.schema().load(body) + if response.status_code == 400: + raise MalformedError(f'Failed to get view data: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to get view data: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to get view data: not found') + 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: + """ + Get data of a table in a database with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + :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. + + :returns: The result of the view query, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If the table does not exist. + :raises QueryStoreError: If the result set could not be counted. + """ + url = f'/api/database/{database_id}/table/{table_id}/data' + params = [] + if page is not None and size is not None: + params.append(('page', page)) + params.append(('size', size)) + if timestamp is not None: + params.append(('timestamp', timestamp)) + response = self._wrapper(method="get", url=url, params=params) + if response.status_code == 200: + body = response.json() + return Result.schema().load(body) + if response.status_code == 400: + raise MalformedError(f'Failed to get table data: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to get table data: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to get table data: not found') + if response.status_code == 409: + raise QueryStoreError(f'Failed to get table data: service rejected result count') + raise ResponseCodeError(f'Failed to get table data: response code: {response.status_code} is not 200 (OK)') + + def create_table_data(self, database_id: int, table_id: int, data: dict) -> None: + """ + Insert data into a table in a database with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + :param data: The data dictionary to be inserted into the table with the form column=value of the table. + + :raises ResponseCodeError: If something went wrong with the insert. + :raises ForbiddenError: If the action is not allowed. + :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). + """ + url = f'/api/database/{database_id}/table/{table_id}/data' + response = self._wrapper(method="post", url=url, force_auth=True, payload=CreateData(data=data)) + if response.status_code == 202: + return + if response.status_code == 400 or response.status_code == 410: + raise MalformedError(f'Failed to insert table data: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to insert table data: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to insert table data: not found') + raise ResponseCodeError( + f'Failed to insert table data: response code: {response.status_code} is not 202 (ACCEPTED)') + + def import_table_data(self, database_id: int, table_id: int, separator: str, file_path: str, + quote: str = None, skip_lines: int = None, false_encoding: str = None, + true_encoding: str = None, null_encoding: str = None, + line_encoding: str = None) -> None: + """ + Import a csv dataset from a file into a table in a database with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + :param separator: The csv column separator. + :param file_path: The path of the file that is imported on the storage service. + :param quote: The column data quotation character. Optional. + :param skip_lines: The number of lines to skip. Optional. + :param false_encoding: The encoding of boolean false. Optional. + :param true_encoding: The encoding of boolean true. Optional. + :param null_encoding: The encoding of null. Optional. Default: empty string "". + :param line_encoding: The encoding of the line termination. Optional. Default: CR (Windows). + + :raises ResponseCodeError: If something went wrong with the insert. + :raises ForbiddenError: If the action is not allowed. + :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) + 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, + skip_lines=skip_lines, false_element=false_encoding, + true_element=true_encoding, null_element=null_encoding, + line_termination=line_encoding)) + if response.status_code == 202: + return + if response.status_code == 400: + raise MalformedError(f'Failed to import table data: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to import table data: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to import table data: not found') + if response.status_code == 409 or response.status_code == 422: + raise ExternalSystemError(f'Failed to import table data: sidecar rejected the import') + raise ResponseCodeError( + f'Failed to import table data: response code: {response.status_code} is not 202 (ACCEPTED)') + + def analyse_datatypes(self, file_path: str, separator: str, enum: bool = None, + enum_tol: int = None, upload: bool = True) -> DatatypeAnalysis: + """ + Import a csv dataset from a file and analyse it for the possible enums, line encoding and column data types. + + :param file_path: The path of the file that is imported on the storage service. + :param separator: The csv column separator. + :param enum: If set to true, enumerations should be guessed, otherwise no guessing. Optional. + :param enum_tol: The tolerance for guessing enumerations (ignored if enum=False). Optional. + :param upload: If set to true, the file from file_path will be uploaded, otherwise no upload will be performed \ + and the file_path will be treated as S3 filename and analysed instead. Optional. Default: true. + + :returns: The determined data types, if successful. + + :raises ResponseCodeError: If something went wrong with the analysis. + :raises MalformedError: If the payload is rejected by the service. + :raises NotExistsError: If the file was not found by the Analyse Service. + """ + if upload: + client = UploadClient(endpoint=self.endpoint) + filename = client.upload(file_path=file_path) + else: + filename = file_path + params = [ + ('filename', filename), + ('separator', separator), + ('enum', enum), + ('enum_tol', enum_tol) + ] + url = f'/api/analyse/datatypes' + response = self._wrapper(method="get", url=url, params=params) + if response.status_code == 202: + body = response.json() + return DatatypeAnalysis.schema().load(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: + raise NotExistsError(f'Failed to analyse data types: failed to find file in Storage Service') + raise ResponseCodeError( + f'Failed to analyse data types: response code: {response.status_code} is not 202 (ACCEPTED)') + + def analyse_keys(self, file_path: str, separator: str, upload: bool = True) -> KeyAnalysis: + """ + Import a csv dataset from a file and analyse it for the possible primary key. + + :param file_path: The path of the file that is imported on the storage service. + :param separator: The csv column separator. + :param upload: If set to true, the file from file_path will be uploaded, otherwise no upload will be performed \ + and the file_path will be treated as S3 filename and analysed instead. Optional. Default: true. + + :returns: The determined ranking of the primary key candidates, if successful. + + :raises ResponseCodeError: If something went wrong with the analysis. + :raises MalformedError: If the payload is rejected by the service. + :raises NotExistsError: If the file was not found by the Analyse Service. + """ + if upload: + client = UploadClient(endpoint=self.endpoint) + filename = client.upload(file_path=file_path) + else: + filename = file_path + params = [ + ('filename', filename), + ('separator', separator), + ] + url = f'/api/analyse/keys' + response = self._wrapper(method="get", url=url, params=params) + if response.status_code == 202: + body = response.json() + return KeyAnalysis.schema().load(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: + raise NotExistsError(f'Failed to analyse data types: failed to find file in Storage Service') + raise ResponseCodeError( + f'Failed to analyse data types: response code: {response.status_code} is not 202 (ACCEPTED)') + + def analyse_table_statistics(self, database_id: int, table_id: int) -> TableStatistics: + """ + Analyses the numerical contents of a table in a database with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + + :returns: The table statistics, if successful. + + :raises ResponseCodeError: If something went wrong with the analysis. + :raises MalformedError: If the payload is rejected by the service. + :raises NotExistsError: If the file was not found by the Analyse Service. + """ + url = f'/api/analyse/database/{database_id}/table/{table_id}/statistics' + response = self._wrapper(method="get", url=url) + if response.status_code == 202: + body = response.json() + return TableStatistics.schema().load(body) + if response.status_code == 400: + raise MalformedError(f'Failed to analyse table statistics: service rejected malformed payload') + if response.status_code == 404: + raise NotExistsError(f'Failed to analyse table statistics: separator error') + raise ResponseCodeError( + f'Failed to analyse table statistics: response code: {response.status_code} is not 202 (ACCEPTED)') + + def update_table_data(self, database_id: int, table_id: int, data: dict, keys: dict) -> None: + """ + Update data in a table in a database with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + :param data: The data dictionary to be updated into the table with the form column=value of the table. + :param keys: The key dictionary matching the rows in the form column=value. + + :raises ResponseCodeError: If something went wrong with the update. + :raises ForbiddenError: If the action is not allowed. + :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). + """ + url = f'/api/database/{database_id}/table/{table_id}/data' + response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateData(data=data, keys=keys)) + if response.status_code == 202: + return + if response.status_code == 400 or response.status_code == 410: + raise MalformedError(f'Failed to update table data: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to update table data: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update table data: not found') + raise ResponseCodeError( + f'Failed to update table data: response code: {response.status_code} is not 202 (ACCEPTED)') + + def delete_table_data(self, database_id: int, table_id: int, keys: dict) -> None: + """ + Delete data in a table in a database with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + :param keys: The key dictionary matching the rows in the form column=value. + + :raises ResponseCodeError: If something went wrong with the deletion. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If the table does not exist. + :raises MalformedError: If the payload is rejected by the service. + """ + url = f'/api/database/{database_id}/table/{table_id}/data' + response = self._wrapper(method="delete", url=url, force_auth=True, payload=DeleteData(keys=keys)) + if response.status_code == 202: + return + if response.status_code == 400: + raise MalformedError(f'Failed to delete table data: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to delete table data: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to delete table data: not found') + raise ResponseCodeError( + f'Failed to delete table data: response code: {response.status_code} is not 202 (ACCEPTED)') + + def get_table_data_count(self, database_id: int, table_id: int, page: int = 0, size: int = 10, + timestamp: datetime.datetime = None) -> int: + """ + Get data count of a table in a database with given database id and table id. + + :param database_id: The database id. + :param table_id: The table id. + :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. + + :returns: The result of the view query, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If the table does not exist. + :raises QueryStoreError: If the result set could not be counted. + """ + url = f'/api/database/{database_id}/table/{table_id}/data' + if page is not None and size is not None: + url += f'?page={page}&size={size}' + if timestamp is not None: + if page is not None and size is not None: + url += '&' + else: + url += '?' + url += f'timestamp={timestamp}' + response = self._wrapper(method="head", url=url) + if response.status_code == 200: + return int(response.headers.get('X-Count')) + if response.status_code == 400: + raise MalformedError(f'Failed to get table data: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to get table data: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to get table data: not found') + if response.status_code == 409: + raise QueryStoreError(f'Failed to get table data: service rejected result count') + raise ResponseCodeError(f'Failed to get table data: response code: {response.status_code} is not 200 (OK)') + + def get_view_data_count(self, database_id: int, view_id: int) -> int: + """ + Get data count of a view in a database with given database id and view id. + + :param database_id: The database id. + :param view_id: The view id. + + :returns: The result count of the view query, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/view/{view_id}/data' + response = self._wrapper(method="head", url=url) + if response.status_code == 200: + return int(response.headers.get('X-Count')) + if response.status_code == 400: + raise MalformedError(f'Failed to get view data count: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to get view data count: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to get view data count: not found') + raise ResponseCodeError(f'Failed to get view data count: response code: {response.status_code} is not 200 (OK)') + + def get_database_access(self, database_id: int) -> AccessType: + """ + Get access of a view in a database with given database id and view id. + + :param database_id: The database id. + + :returns: The access type, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thecontainer does not exist. + """ + url = f'/api/database/{database_id}/access' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return DatabaseAccess.schema().load(body).type + if response.status_code == 403: + raise ForbiddenError(f'Failed to get database access: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to get database access: not found') + raise ResponseCodeError(f'Failed to get database access: response code: {response.status_code} is not 200 (OK)') + + def create_database_access(self, database_id: int, user_id: str, type: AccessType) -> AccessType: + """ + Create access to a database with given database id and user id. + + :param database_id: The database id. + :param user_id: The user id. + :param type: The access type. + + :returns: The access type, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedatabase or user does not exist. + """ + url = f'/api/database/{database_id}/access/{user_id}' + response = self._wrapper(method="post", url=url, force_auth=True, payload=CreateAccess(type=type)) + if response.status_code == 202: + body = response.json() + return DatabaseAccess.schema().load(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: + raise ForbiddenError(f'Failed to create database access: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to create database access: not found') + raise ResponseCodeError( + f'Failed to create database access: response code: {response.status_code} is not 202 (ACCEPTED)') + + def update_database_access(self, database_id: int, user_id: str, type: AccessType) -> AccessType: + """ + Updates the access for a user to a database with given database id and user id. + + :param database_id: The database id. + :param user_id: The user id. + :param type: The access type. + + :returns: The access type, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedatabase or user does not exist. + """ + url = f'/api/database/{database_id}/access/{user_id}' + response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateAccess(type=type)) + if response.status_code == 202: + body = response.json() + return DatabaseAccess.schema().load(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: + raise ForbiddenError(f'Failed to update database access: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update database access: not found') + raise ResponseCodeError( + f'Failed to update database access: response code: {response.status_code} is not 202 (ACCEPTED)') + + def delete_database_access(self, database_id: int, user_id: str) -> None: + """ + Deletes the access for a user to a database with given database id and user id. + + :param database_id: The database id. + :param user_id: The user id. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedatabase or user does not exist. + """ + url = f'/api/database/{database_id}/access/{user_id}' + response = self._wrapper(method="delete", url=url, force_auth=True) + if response.status_code == 202: + return + if response.status_code == 400: + raise MalformedError(f'Failed to delete database access: service rejected malformed payload') + if response.status_code == 403 or response.status_code == 405: + raise ForbiddenError(f'Failed to delete database access: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to delete database access: not found') + raise ResponseCodeError( + f'Failed to delete database access: response code: {response.status_code} is not 202 (ACCEPTED)') + + def execute_query(self, database_id: int, query: str, page: int = 0, size: int = 10, + timestamp: datetime.datetime = None) -> Result: + """ + Executes a SQL query in a database where the current user has at least read access with given database id. The + result set can be paginated with setting page and size (both). Historic data can be queried by setting + timestamp. + + :param database_id: The database id. + :param query: The query statement. + :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. + + :returns: The result set, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedatabase, table or user does not exist. + :raises QueryStoreError: The query store rejected the query. + :raises MetadataConsistencyError: The service failed to parse columns from the metadata database. + """ + url = f'/api/database/{database_id}/query' + if page is not None and size is not None: + url += f'?page={page}&size={size}' + response = self._wrapper(method="post", url=url, force_auth=True, + payload=ExecuteQuery(statement=query, timestamp=timestamp)) + if response.status_code == 202: + body = response.json() + return Result.schema().load(body) + if response.status_code == 400: + raise MalformedError(f'Failed to execute query: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to execute query: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to execute query: not found') + if response.status_code == 409: + raise QueryStoreError(f'Failed to execute query: query store rejected query') + if response.status_code == 417: + raise MetadataConsistencyError(f'Failed to execute query: service expected other metadata') + raise ResponseCodeError( + 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: + """ + Re-executes a query in a database with given database id and query id. + + :param database_id: The database id. + :param query_id: The query id. + :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. + + :returns: The result set, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If the database, query or user does not exist. + :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) + if response.status_code == 200: + if file_path is None: + body = response.json() + return Result.schema().load(body) + else: + with open(file_path, "w") as f: + f.write(response.content.decode("utf-8")) + 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: + raise ForbiddenError(f'Failed to re-execute query: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to re-execute query: not found') + if response.status_code == 409: + raise QueryStoreError(f'Failed to re-execute query: query store rejected query') + if response.status_code == 417: + raise MetadataConsistencyError(f'Failed to re-execute query: service expected other metadata') + raise ResponseCodeError( + f'Failed to re-execute query: response code: {response.status_code} is not 200 (OK)') + + def get_query_data_count(self, database_id: int, query_id: int, page: int = 0, size: int = 10) -> int: + """ + Re-executes a query in a database with given database id and query id and only counts the results. + + :param database_id: The database id. + :param query_id: The query id. + :param page: The result pagination number. Optional. Default: 0. + :param size: The result pagination size. Optional. Default: 10. + + :returns: The result set, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If the database, query or user does not exist. + :raises QueryStoreError: The query store rejected the query. + :raises MetadataConsistencyError: The service failed to parse columns from the metadata database. + """ + 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}' + response = self._wrapper(method="head", url=url) + if response.status_code == 200: + return int(response.headers.get('X-Count')) + 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: + raise ForbiddenError(f'Failed to re-execute query: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to re-execute query: not found') + if response.status_code == 409: + raise QueryStoreError(f'Failed to re-execute query: query store rejected query') + if response.status_code == 417: + raise MetadataConsistencyError(f'Failed to re-execute query: service expected other metadata') + raise ResponseCodeError( + f'Failed to re-execute query: response code: {response.status_code} is not 200 (OK)') + + def get_query(self, database_id: int, query_id: int) -> Query: + """ + Get query from a database with given database id and query id. + + :param database_id: The database id. + :param query_id: The query id. + + :returns: The query, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedatabase, query or user does not exist. + :raises QueryStoreError: The query store rejected the query. + :raises MetadataConsistencyError: The service failed to parse columns from the metadata database. + """ + url = f'/api/database/{database_id}/query/{query_id}' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return Query.schema().load(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: + raise ForbiddenError(f'Failed to find query: not allowed') + if response.status_code == 417: + raise MetadataConsistencyError(f'Failed to find query: service expected other metadata') + if response.status_code == 501 or response.status_code == 503 or response.status_code == 504: + raise QueryStoreError(f'Failed to find query: query store rejected query') + raise ResponseCodeError( + f'Failed to find query: response code: {response.status_code} is not 200 (OK)') + + def get_queries(self, database_id: int) -> List[Query]: + """ + Get queries from a database with given database id. + + :param database_id: The database id. + + :returns: List of queries, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises MalformedError: If the query is rejected by the service. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedatabase or user does not exist. + :raises QueryStoreError: The query store rejected the query. + """ + url = f'/api/database/{database_id}/query' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return Query.schema().load(body, many=True) + if response.status_code == 403 or response.status_code == 405: + raise ForbiddenError(f'Failed to find queries: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to find queries: not found') + if response.status_code == 423: + raise MalformedError(f'Failed to find queries: service rejected malformed query') + if response.status_code == 501 or response.status_code == 503 or response.status_code == 504: + raise QueryStoreError(f'Failed to find queries: query store rejected query') + raise ResponseCodeError( + f'Failed to find query: response code: {response.status_code} is not 200 (OK)') + + def update_query(self, database_id: int, query_id: int, persist: bool) -> Query: + """ + Update query from a database with given database id and query id. + + :param database_id: The database id. + :param query_id: The query id. + :param persist: If set to true, the query will be saved and visible in the User Interface, otherwise the query \ + is marked for deletion in the future and not visible in the User Interface. + + :returns: The query, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + :raises ForbiddenError: If the action is not allowed. + :raises NotExistsError: If thedatabase or user does not exist. + :raises QueryStoreError: The query store rejected the update. + """ + url = f'/api/database/{database_id}/query/{query_id}' + response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateQuery(persist=persist)) + if response.status_code == 202: + body = response.json() + return Query.schema().load(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: + raise NotExistsError(f'Failed to update query: not found') + if response.status_code == 412: + raise QueryStoreError(f'Failed to update query: query store rejected update') + raise ResponseCodeError( + f'Failed to update query: response code: {response.status_code} is not 200 (OK)') + + def create_identifier(self, database_id: int, type: IdentifierType, titles: List[CreateIdentifierTitle], + publisher: str, creators: List[CreateIdentifierCreator], publication_year: int, + descriptions: List[CreateIdentifierDescription] = None, + funders: List[CreateIdentifierFunder] = None, licenses: List[License] = None, + language: Language = None, query_id: int = None, view_id: int = None, table_id: int = None, + publication_day: int = None, publication_month: int = None, + related_identifiers: List[CreateRelatedIdentifier] = None) -> Identifier: + """ + Create an identifier + + :param database_id: The database id of the created identifier. + :param type: The type of the created identifier. + :param titles: The titles of the created identifier. + :param publisher: The publisher of the created identifier. + :param creators: The creator(s) of the created identifier. + :param publication_year: The publication year of the created identifier. + :param descriptions: The description(s) of the created identifier. Optional. + :param funders: The funders(s) of the created identifier. Optional. + :param licenses: The license(s) of the created identifier. Optional. + :param language: The language of the created identifier. Optional. + :param query_id: The query id of the created identifier. Required when type=SUBSET, otherwise invalid. Optional. + :param view_id: The view id of the created identifier. Required when type=VIEW, otherwise invalid. Optional. + :param table_id: The table id of the created identifier. Required when type=TABLE, otherwise invalid. Optional. + :param publication_day: The publication day of the created identifier. Optional. + :param publication_month: The publication month of the created identifier. Optional. + :param related_identifiers: The related identifier(s) of the created identifier. Optional. + + :returns: The identifier, if successful. + + :raises ResponseCodeError: If something went wrong with the creation of the identifier. + :raises ForbiddenError: If the action is not allowed. + :raises MalformedError: If the payload is rejected by the service. + :raises NotExistsError: If the database, table/view/query or user does not exist. + :raises ExternalSystemError: If the external system (DataCite) refused communication with the service. + """ + url = f'/api/identifier' + payload = CreateIdentifier(database_id=database_id, type=type, titles=titles, publisher=publisher, + creators=creators, publication_year=publication_year, descriptions=descriptions, + funders=funders, licenses=licenses, language=language, query_id=query_id, + view_id=view_id, table_id=table_id, publication_day=publication_day, + publication_month=publication_month, related_identifiers=related_identifiers) + response = self._wrapper(method="post", url=url, force_auth=True, payload=payload) + if response.status_code == 201: + body = response.json() + return Identifier.schema().load(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: + raise ForbiddenError(f'Failed to create identifier: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to create identifier: not found') + if response.status_code == 503: + raise ExternalSystemError(f'Failed to create identifier: external system rejected communication') + raise ResponseCodeError( + f'Failed to create identifier: response code: {response.status_code} is not 201 (CREATED)') + + def suggest_identifier(self, uri: str) -> Identifier: + """ + Suggest identifier metadata for a given identifier URI. Example: ROR, ORCID, ISNI, GND, DOI. + + :param uri: The identifier URI. + + :returns: The identifier, if successful. + + :raises ResponseCodeError: If something went wrong with the suggestion of the identifier. + :raises NotExistsError: If no metadata can be found or the identifier type is not supported. + """ + url = f'/api/identifier?url={uri}' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return Identifier.schema().load(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)') + + def get_licenses(self) -> List[License]: + """ + Get list of licenses allowed. + + :returns: List of licenses, if successful. + """ + url = f'/api/database/license' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return License.schema().load(body, many=True) + raise ResponseCodeError(f'Failed to get licenses: response code: {response.status_code} is not 200 (OK)') + + def get_identifiers(self, ld: bool = False) -> List[Identifier] | str: + """ + Get list of identifiers. + + :param ld: If set to true, identifiers are requested as JSON-LD. Optional. Default: false. + + :returns: List of identifiers, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval of the identifiers. + :raises NotExistsError: If the accept header is neither application/json nor application/ld+json. + """ + url = f'/api/pid' + headers = None + if ld: + headers = {'Accept': 'application/ld+json'} + response = self._wrapper(method="get", url=url, headers=headers) + if response.status_code == 200: + if ld: + return response.json() + else: + body = response.json() + return Identifier.schema().load(body, many=True) + if response.status_code == 406: + raise MalformedError( + f'Failed to get identifiers: accept header must be application/json or application/ld+json') + raise ResponseCodeError(f'Failed to get identifiers: response code: {response.status_code} is not 200 (OK)') + + def update_table_column(self, database_id: int, table_id: int, column_id: int, concept_uri: str = None, + unit_uri: str = None) -> Column: + """ + Update semantic information of a table column by given database id and table id and column id. + + :param database_id: The database id. + :param table_id: The table id. + :param column_id: The column id. + :param concept_uri: The concept URI. Optional. + :param unit_uri: The unit URI. Optional. + + :returns: The column, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval of the identifiers. + :raises NotExistsError: If the accept header is neither application/json nor application/ld+json. + """ + url = f'/api/database/{database_id}/table/{table_id}/column/{column_id}' + response = self._wrapper(method="put", url=url, force_auth=True, + payload=UpdateColumn(concept_uri=concept_uri, unit_uri=unit_uri)) + if response.status_code == 202: + body = response.json() + return Column.schema().load(body) + if response.status_code == 400: + raise MalformedError(f'Failed to update column: service rejected malformed payload') + if response.status_code == 403: + raise ForbiddenError(f'Failed to update colum: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update colum: not found') + raise ResponseCodeError(f'Failed to update colum: response code: {response.status_code} is not 202 (ACCEPTED)') diff --git a/dbrepo-analyse-service/test/__init__.py b/lib/python/dbrepo/__init__.py similarity index 100% rename from dbrepo-analyse-service/test/__init__.py rename to lib/python/dbrepo/__init__.py diff --git a/Pipfile b/lib/python/dbrepo/api/__init__.py similarity index 100% rename from Pipfile rename to lib/python/dbrepo/api/__init__.py diff --git a/lib/python/dbrepo/api/dto.py b/lib/python/dbrepo/api/dto.py new file mode 100644 index 0000000000..28b74db963 --- /dev/null +++ b/lib/python/dbrepo/api/dto.py @@ -0,0 +1,1096 @@ +from __future__ import annotations + +import datetime +from enum import Enum +from typing import List, Optional +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass(init=True, eq=True) +class ImageDate: + id: int + example: str + database_format: str + unix_format: str + has_time: bool + created_at: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class Image: + id: int + registry: str + name: str + version: str + dialect: str + driver_class: str + jdbc_method: str + default_port: int + date_formats: Optional[List[ImageDate]] = field(default_factory=list) + + +@dataclass_json +@dataclass(init=True, eq=True) +class ImageBrief: + id: int + name: str + version: str + jdbc_method: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateDatabase: + name: int + container_id: int + is_public: bool + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateUser: + username: str + email: str + password: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class UpdateUser: + firstname: str + lastname: str + affiliation: str + orcid: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class UserBrief: + id: str + username: str + name: str + orcid: str + qualified_name: str + given_name: str + family_name: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class Container: + id: int + name: str + host: str + port: int + image: Image + created: str + internal_name: str + sidecar_host: str + sidecar_port: int + ui_host: Optional[str] = None + ui_port: Optional[int] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class ContainerBrief: + id: int + name: str + image: ImageBrief + created: str + internal_name: str + running: Optional[bool] = None + hash: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class ColumnBrief: + id: int + name: str + alias: str + database_id: int + table_id: int + internal_name: str + column_type: ColumnType + + +@dataclass_json +@dataclass(init=True, eq=True) +class TableBrief: + id: int + name: str + description: str + owner: UserBrief + columns: List[ColumnBrief] + internal_name: str + is_versioned: bool + + +@dataclass_json +@dataclass(init=True, eq=True) +class UserAttributes: + theme: str + orcid: Optional[str] = None + affiliation: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class User: + id: str + username: str + attributes: UserAttributes + qualified_name: Optional[str] = None + given_name: Optional[str] = None + family_name: Optional[str] = None + name: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class UpdateUserTheme: + theme: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class UpdateUserPassword: + password: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class UserBrief: + id: str + username: str + name: Optional[str] = None + orcid: Optional[str] = None + qualified_name: Optional[str] = None + given_name: Optional[str] = None + family_name: Optional[str] = None + + +class AccessType(str, Enum): + """ + Enumeration of database access. + """ + READ = "read" + """The user can read all data.""" + + WRITE_OWN = "write_own" + """The user can write into self-owned tables and read all data.""" + + WRITE_ALL = "write_all" + """The user can write in all tables and read all data.""" + + +class ColumnType(str, Enum): + """ + Enumeration of table column data types. + """ + CHAR = "char" + VARCHAR = "varchar" + BINARY = "binary" + VARBINARY = "varbinary" + TINYBLOB = "tinyblob" + TINYTEXT = "tinytext" + TEXT = "text" + BLOB = "blob" + MEDIUMTEXT = "mediumtext" + MEDIUMBLOB = "mediumblob" + LONGTEXT = "longtext" + LONGBLOB = "longblob" + ENUM = "enum" + SET = "set" + BIT = "bit" + TINYINT = "tinyint" + BOOL = "bool" + SMALLINT = "smallint" + MEDIUMINT = "mediumint" + INT = "int" + BIGINT = "bigint" + FLOAT = "float" + DOUBLE = "double" + DECIMAL = "decimal" + DATE = "date" + DATETIME = "datetime" + TIMESTAMP = "timestamp" + TIME = "time" + YEAR = "year" + + +class Language(str, Enum): + """ + Enumeration of languages. + """ + AB = "ab" + AA = "aa" + AF = "af" + AK = "ak" + SQ = "sq" + AM = "am" + AR = "ar" + AN = "an" + HY = "hy" + AS = "as" + AV = "av" + AE = "ae" + AY = "ay" + AZ = "az" + BM = "bm" + BA = "ba" + EU = "eu" + BE = "be" + BN = "bn" + BH = "bh" + BI = "bi" + BS = "bs" + BR = "br" + BG = "bg" + MY = "my" + CA = "ca" + KM = "km" + CH = "ch" + CE = "ce" + NY = "ny" + ZH = "zh" + CU = "cu" + CV = "cv" + KW = "kw" + CO = "co" + CR = "cr" + HR = "hr" + CS = "cs" + DA = "da" + DV = "dv" + NL = "nl" + DZ = "dz" + EN = "en" + EO = "eo" + ET = "et" + EE = "ee" + FO = "fo" + FJ = "fj" + FI = "fi" + FR = "fr" + FF = "ff" + GD = "gd" + GL = "gl" + LG = "lg" + KA = "ka" + DE = "de" + KI = "ki" + EL = "el" + KL = "kl" + GN = "gn" + GU = "gu" + HT = "ht" + HA = "ha" + HE = "he" + HZ = "hz" + HI = "hi" + HO = "ho" + HU = "hu" + IS = "is" + IO = "io" + IG = "ig" + ID = "id" + IA = "ia" + IE = "ie" + IU = "iu" + IK = "ik" + GA = "ga" + IT = "it" + JA = "ja" + JV = "jv" + KN = "kn" + KR = "kr" + KS = "ks" + KK = "kk" + RW = "rw" + KV = "kv" + KG = "kg" + KO = "ko" + KJ = "kj" + KU = "ku" + KY = "ky" + LO = "lo" + LA = "la" + LV = "lv" + LB = "lb" + LI = "li" + LN = "ln" + LT = "lt" + LU = "lu" + MK = "mk" + MG = "mg" + MS = "ms" + ML = "ml" + MT = "mt" + GV = "gv" + MI = "mi" + MR = "mr" + MH = "mh" + RO = "ro" + MN = "mn" + NA = "na" + NV = "nv" + ND = "nd" + NG = "ng" + NE = "ne" + SE = "se" + NO = "no" + NB = "nb" + NN = "nn" + II = "ii" + OC = "oc" + OJ = "oj" + OR = "or" + OM = "om" + OS = "os" + PI = "pi" + PA = "pa" + PS = "ps" + FA = "fa" + PL = "pl" + PT = "pt" + QU = "qu" + RM = "rm" + RN = "rn" + RU = "ru" + SM = "sm" + SG = "sg" + SA = "sa" + SC = "sc" + SR = "sr" + SN = "sn" + SD = "sd" + SI = "si" + SK = "sk" + SL = "sl" + SO = "so" + ST = "st" + NR = "nr" + ES = "es" + SU = "su" + SW = "sw" + SS = "ss" + SV = "sv" + TL = "tl" + TY = "ty" + TG = "tg" + TA = "ta" + TT = "tt" + TE = "te" + TH = "th" + BO = "bo" + TI = "ti" + TO = "to" + TS = "ts" + TN = "tn" + TR = "tr" + TK = "tk" + TW = "tw" + UG = "ug" + UK = "uk" + UR = "ur" + UZ = "uz" + VE = "ve" + VI = "vi" + VO = "vo" + WA = "wa" + CY = "cy" + FY = "fy" + WO = "wo" + XH = "xh" + YI = "yi" + YO = "yo" + ZA = "za" + ZU = "zu" + + +@dataclass_json +@dataclass(init=True, eq=True) +class DatabaseAccess: + type: AccessType + user: User + created: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateAccess: + type: AccessType + + +@dataclass_json +@dataclass(init=True, eq=True) +class UpdateAccess: + type: AccessType + + +@dataclass_json +@dataclass(init=True, eq=True) +class IdentifierTitle: + id: int + title: str + language: Optional[Language] = None + type: Optional[TitleType] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateIdentifierTitle: + title: str + language: Optional[Language] = None + type: Optional[TitleType] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class IdentifierDescription: + id: int + description: str + language: Optional[Language] = None + type: Optional[DescriptionType] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateIdentifierDescription: + description: str + language: Optional[Language] = None + type: Optional[DescriptionType] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class IdentifierFunder: + id: int + funder_name: str + funder_identifier: Optional[str] = None + funder_identifier_type: Optional[str] = None + scheme_uri: Optional[str] = None + award_number: Optional[str] = None + award_title: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateIdentifierFunder: + funder_name: str + funder_identifier: Optional[str] = None + funder_identifier_type: Optional[str] = None + scheme_uri: Optional[str] = None + award_number: Optional[str] = None + award_title: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class License: + identifier: str + uri: str + description: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateData: + data: dict + + +@dataclass_json +@dataclass(init=True, eq=True) +class UpdateData: + data: dict + keys: dict + + +@dataclass_json +@dataclass(init=True, eq=True) +class DeleteData: + keys: dict + + +@dataclass_json +@dataclass(init=True, eq=True) +class Import: + location: str + separator: str + quote: Optional[str] = None + skip_lines: Optional[int] = None + false_element: Optional[bool] = None + true_element: Optional[bool] = None + null_element: Optional[str] = None + line_termination: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class UpdateColumn: + concept_uri: Optional[str] = None + unit_uri: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class ModifyVisibility: + is_public: bool + + +@dataclass_json +@dataclass(init=True, eq=True) +class ModifyOwner: + id: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateTable: + name: str + constraints: CreateTableConstraints + columns: List[CreateTableColumn] = field(default_factory=list) + description: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateTableColumn: + name: str + type: ColumnType + primary_key: bool + null_allowed: bool + index_length: Optional[int] = None + size: Optional[int] = None + d: Optional[int] = None + dfid: Optional[int] = None + enums: Optional[List[str]] = None + sets: Optional[List[str]] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateTableConstraints: + uniques: List[List[str]] = field(default_factory=list) + checks: List[str] = field(default_factory=list) + foreign_keys: List[CreateForeignKey] = field(default_factory=list) + + +@dataclass_json +@dataclass(init=True, eq=True) +class IdentifierCreator: + id: int + creator_name: str + firstname: Optional[str] = None + lastname: Optional[str] = None + affiliation: Optional[str] = None + name_type: Optional[str] = None + name_identifier: Optional[str] = None + name_identifier_scheme: Optional[str] = None + name_identifier_scheme_uri: Optional[str] = None + affiliation_identifier: Optional[str] = None + affiliation_identifier_scheme: Optional[str] = None + affiliation_identifier_scheme_uri: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateIdentifierCreator: + creator_name: str + firstname: Optional[str] = None + lastname: Optional[str] = None + affiliation: Optional[str] = None + name_type: Optional[str] = None + name_identifier: Optional[str] = None + name_identifier_scheme: Optional[str] = None + name_identifier_scheme_uri: Optional[str] = None + affiliation_identifier: Optional[str] = None + affiliation_identifier_scheme: Optional[str] = None + affiliation_identifier_scheme_uri: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class RelatedIdentifier: + id: int + value: str + type: RelatedIdentifierType + relation: RelatedIdentifierRelation + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateRelatedIdentifier: + value: str + type: RelatedIdentifierType + relation: RelatedIdentifierRelation + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateIdentifier: + type: IdentifierType + creators: List[CreateIdentifierCreator] + publication_year: int + titles: Optional[List[CreateIdentifierTitle]] = field(default_factory=list) + descriptions: Optional[List[CreateIdentifierTitle]] = field(default_factory=list) + funders: Optional[List[CreateIdentifierFunder]] = field(default_factory=list) + doi: Optional[str] = None + publisher: Optional[str] = None + language: Optional[str] = None + licenses: Optional[List[License]] = field(default_factory=list) + database_id: Optional[int] = None + query_id: Optional[int] = None + table_id: Optional[int] = None + view_id: Optional[int] = None + query: Optional[str] = None + query_normalized: Optional[str] = None + execution: Optional[str] = None + related_identifiers: Optional[List[CreateRelatedIdentifier]] = field(default_factory=list) + result_hash: Optional[str] = None + result_number: Optional[int] = None + publication_day: Optional[int] = None + publication_month: Optional[int] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class Identifier: + id: int + type: IdentifierType + creators: List[IdentifierCreator] + created: str + publication_year: int + last_modified: str + titles: Optional[List[IdentifierTitle]] = field(default_factory=list) + descriptions: Optional[List[IdentifierDescription]] = field(default_factory=list) + funders: Optional[List[IdentifierFunder]] = field(default_factory=list) + doi: Optional[str] = None + publisher: Optional[str] = None + language: Optional[str] = None + licenses: Optional[List[License]] = field(default_factory=list) + database_id: Optional[int] = None + query_id: Optional[int] = None + table_id: Optional[int] = None + view_id: Optional[int] = None + query: Optional[str] = None + query_normalized: Optional[str] = None + execution: Optional[str] = None + related_identifiers: Optional[List[RelatedIdentifier]] = field(default_factory=list) + result_hash: Optional[str] = None + result_number: Optional[int] = None + publication_day: Optional[int] = None + publication_month: Optional[int] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class View: + id: int + database_id: int + name: str + query: str + query_hash: str + created: str + creator: User + internal_name: str + is_public: bool + initial_view: bool + last_modified: str + identifiers: List[Identifier] = field(default_factory=list) + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateView: + name: str + query: str + is_public: bool + + +@dataclass_json +@dataclass(init=True, eq=True) +class Result: + result: List[any] + headers: List[str] + id: Optional[int] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class ViewBrief: + id: int + database_id: int + name: str + identifier: List[Identifier] + query: str + query_hash: str + created: str + creator: User + internal_name: str + is_public: bool + initial_view: bool + last_modified: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class ColumnBrief: + id: int + name: str + alias: str + database_id: int + table_id: int + internal_name: str + column_type: str + + +@dataclass_json +@dataclass(init=True, eq=True) +class Concept: + id: int + uri: str + created: str + columns: List[ColumnBrief] = field(default_factory=list) + name: Optional[str] = None + description: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class DatatypeAnalysis: + separator: str + columns: dict[str, ColumnType] + line_termination: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class KeyAnalysis: + keys: dict[str, int] + + +@dataclass_json +@dataclass(init=True, eq=True) +class ColumnStatistic: + val_min: float + val_max: float + mean: float + median: float + std_dev: float + + +@dataclass_json +@dataclass(init=True, eq=True) +class TableStatistics: + columns: dict[str, ColumnStatistic] + + +@dataclass_json +@dataclass(init=True, eq=True) +class Unit: + id: int + uri: str + created: str + columns: List[ColumnBrief] = field(default_factory=list) + name: Optional[str] = None + description: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class ExecuteQuery: + statement: str + timestamp: datetime.datetime + + +class TitleType(str, Enum): + """ + Enumeration of identifier title types. + """ + ALTERNATIVE_TITLE = "AlternativeTitle" + SUBTITLE = "Subtitle" + TRANSLATED_TITLE = "TranslatedTitle" + OTHER = "Other" + + +class RelatedIdentifierType(str, Enum): + """ + Enumeration of related identifier types. + """ + DOI = "DOI" + URL = "URL" + URN = "URN" + ARK = "ARK" + ARXIV = "arXiv" + BIBCODE = "bibcode" + EAN13 = "EAN13" + EISSN = "EISSN" + HANDLE = "Handle" + IGSN = "IGSN" + ISBN = "ISBN" + ISTC = "ISTC" + LISSN = "LISSN" + LSID = "LSID" + PMID = "PMID" + PURL = "PURL" + UPC = "UPC" + W3ID = "w3id" + + +class RelatedIdentifierRelation(str, Enum): + """ + Enumeration of related identifier types. + """ + IS_CITED_BY = "IsCitedBy" + CITES = "Cites" + IS_SUPPLEMENT_TO = "IsSupplementTo" + IS_SUPPLEMENTED_BY = "IsSupplementedBy" + IS_CONTINUED_BY = "IsContinuedBy" + CONTINUES = "Continues" + IS_DESCRIBED_BY = "IsDescribedBy" + DESCRIBES = "Describes" + HAS_METADATA = "HasMetadata" + IS_METADATA_FOR = "IsMetadataFor" + HAS_VERSION = "HasVersion" + IS_VERSION_OF = "IsVersionOf" + IS_NEW_VERSION_OF = "IsNewVersionOf" + IS_PREVIOUS_VERSION_OF = "IsPreviousVersionOf" + IS_PART_OF = "IsPartOf" + HAS_PART = "HasPart" + IS_PUBLISHED_IN = "IsPublishedIn" + IS_REFERENCED_BY = "IsReferencedBy" + REFERENCES = "References" + IS_DOCUMENTED_BY = "IsDocumentedBy" + DOCUMENTS = "Documents" + IS_COMPILED_BY = "IsCompiledBy" + COMPILES = "Compiles" + IS_VARIANT_FORM_OF = "IsVariantFormOf" + IS_ORIGINAL_FORM_OF = "IsOriginalFormOf" + IS_IDENTICAL_TO = "IsIdenticalTo" + IS_REVIEWED_BY = "IsReviewedBy" + REVIEWS = "Reviews" + IS_DERIVED_FROM = "IsDerivedFrom" + IS_SOURCE_OF = "IsSourceOf" + IS_REQUIRED_BY = "IsRequiredBy" + REQUIRES = "Requires" + IS_OBSOLETED_BY = "IsObsoletedBy" + OBSOLETES = "Obsoletes" + + +class DescriptionType(str, Enum): + """ + Enumeration of identifier description types. + """ + ABSTRACT = "Abstract" + METHODS = "Methods" + SERIES_INFORMATION = "SeriesInformation" + TABLE_OF_CONTENTS = "TableOfContents" + TECHNICAL_INFO = "TechnicalInfo" + OTHER = "Other" + + +@dataclass_json +@dataclass(init=True, eq=True) +class IdentifierTitle: + """ + Title of an identifier. See external documentation: https://support.datacite.org/docs/datacite-metadata-schema-v44-mandatory-properties#3-title. + """ + id: int + title: str + language: Optional[str] = None + type: Optional[str] = None + + +class QueryType(str, Enum): + """ + Enumeration of query types. + """ + VIEW = "view" + """The query was executed as part of a view.""" + + QUERY = "query" + """The query was executed as subset.""" + + +class IdentifierType(str, Enum): + """ + Enumeration of identifier types. + """ + VIEW = "view" + """The identifier is identifying a view.""" + + SUBSET = "subset" + """The identifier is identifying a subset.""" + + DATABASE = "database" + """The identifier is identifying a database.""" + + TABLE = "table" + """The identifier is identifying a table.""" + + +class IdentifierType(str, Enum): + """ + Enumeration of identifier types. + """ + TABLE = "table" + """The identifier identifies a table.""" + + DATABASE = "database" + """The identifier identifies a database.""" + + VIEW = "view" + """The identifier identifies a view.""" + + SUBSET = "subset" + """The identifier identifies a subset.""" + + +@dataclass_json +@dataclass(init=True, eq=True) +class Query: + id: int + creator: User + execution: str + query: str + type: QueryType + created: str + database_id: int + query_hash: str + is_persisted: bool + result_hash: str + query_normalized: str + last_modified: str + result_number: Optional[int] = None + identifiers: List[Identifier] = field(default_factory=list) + + +@dataclass_json +@dataclass(init=True, eq=True) +class UpdateQuery: + persist: bool + + +@dataclass_json +@dataclass(init=True, eq=True) +class Column: + id: int + name: str + database_id: int + table_id: int + internal_name: str + auto_generated: bool + is_primary_key: bool + column_type: ColumnType + is_public: bool + is_null_allowed: bool + alias: Optional[str] = None + size: Optional[int] = None + d: Optional[int] = None + mean: Optional[float] = None + median: Optional[float] = None + concept: Optional[Concept] = None + unit: Optional[Unit] = None + enums: Optional[List[str]] = field(default_factory=list) + sets: Optional[List[str]] = field(default_factory=list) + date_format: Optional[ImageDate] = None + index_length: Optional[int] = None + length: Optional[int] = None + data_length: Optional[int] = None + max_data_length: Optional[int] = None + num_rows: Optional[int] = None + val_min: Optional[float] = None + val_max: Optional[float] = None + std_dev: Optional[float] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class Table: + id: int + database_id: int + name: str + creator: User + owner: User + created: str + columns: List[Column] + constraints: Constraints + internal_name: str + is_versioned: bool + created_by: str + queue_name: str + routing_key: str + is_public: bool + identifiers: Optional[List[Identifier]] = field(default_factory=list) + description: Optional[str] = None + queue_type: Optional[str] = None + num_rows: Optional[int] = None + data_length: Optional[int] = None + max_data_length: Optional[int] = None + avg_row_length: Optional[int] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class Database: + id: int + name: str + creator: User + owner: User + contact: User + created: str + exchange_name: str + internal_name: str + is_public: bool + container: Container + identifiers: Optional[List[Identifier]] = field(default_factory=list) + subsets: Optional[List[Identifier]] = field(default_factory=list) + description: Optional[str] = None + tables: Optional[List[Table]] = field(default_factory=list) + views: Optional[List[View]] = field(default_factory=list) + image: Optional[str] = None + accesses: Optional[List[DatabaseAccess]] = field(default_factory=list) + exchange_type: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class Unique: + uid: int + table: Table + columns: List[Column] + + +@dataclass_json +@dataclass(init=True, eq=True) +class ForeignKey: + name: str + columns: List[Column] + referenced_table: Table + referenced_columns: List[Column] + on_update: Optional[str] = None + on_delete: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class CreateForeignKey: + columns: List[Column] + referenced_table: Table + referenced_columns: List[Column] + on_update: Optional[str] = None + on_delete: Optional[str] = None + + +@dataclass_json +@dataclass(init=True, eq=True) +class Constraints: + uniques: Optional[List[Unique]] = None + foreign_keys: Optional[List[ForeignKey]] = None + checks: Optional[List[str]] = None diff --git a/lib/python/dbrepo/api/exceptions.py b/lib/python/dbrepo/api/exceptions.py new file mode 100644 index 0000000000..a606a4fc7b --- /dev/null +++ b/lib/python/dbrepo/api/exceptions.py @@ -0,0 +1,82 @@ +class ResponseCodeError(Exception): + """ + The response code is different from the expected one. + """ + pass + + +class UsernameExistsError(Exception): + """ + The username is already in use by another user. + """ + pass + + +class EmailExistsError(Exception): + """ + The e-mail address is already in use by another user. + """ + pass + + +class NameExistsError(Exception): + """ + The resource with this name exists. + """ + pass + + +class NotExistsError(Exception): + """ + The resource was not found. + """ + pass + + +class ForbiddenError(Exception): + """ + The action is not allows. + """ + pass + + +class MalformedError(Exception): + """ + The data is malformed. + """ + pass + + +class QueryStoreError(Exception): + """ + The data is malformed. + """ + pass + + +class MetadataConsistencyError(Exception): + """ + The service expected metadata that is not present. + """ + pass + + +class ExternalSystemError(Exception): + """ + The service could not communicate with the external system. + """ + pass + + +class AuthenticationError(Exception): + """ + The action requires authentication. + """ + pass + + +class UploadError(Exception): + """ + The upload was not successful. + """ + pass diff --git a/lib/python/debug.py b/lib/python/debug.py new file mode 100644 index 0000000000..433a9e57ee --- /dev/null +++ b/lib/python/debug.py @@ -0,0 +1,6 @@ +from dbrepo.RestClient import RestClient +from python.dbrepo.api.dto import AccessType + +client = RestClient(endpoint="https://test.dbrepo.tuwien.ac.at", username="foo", + password="bar") +client.delete_database_access() diff --git a/lib/python/docs/Makefile b/lib/python/docs/Makefile new file mode 100644 index 0000000000..d4bb2cbb9e --- /dev/null +++ b/lib/python/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/lib/python/docs/conf.py b/lib/python/docs/conf.py new file mode 100644 index 0000000000..4d955a3a4c --- /dev/null +++ b/lib/python/docs/conf.py @@ -0,0 +1,54 @@ +import os +import sys +import datetime + +sys.path.insert(0, os.path.abspath("..")) +sys.path.append(os.path.abspath("../dbrepo")) + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "dbrepo" +current_year = datetime.date.today().year +copyright = f'{current_year} the DBRepo Developers' +author = "Martin Weise" +release = "__APPVERSION__" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.napoleon"] +templates_path = ["templates"] +exclude_patterns = ["build", "Thumbs.db", ".DS_Store", "debug.py", "setup.py"] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "furo" +html_title = f"DBRepo Python Library {release} documentation" +html_logo_width = 200 +html_static_path = ["static"] +html_theme_options = { + "light_logo": "theme_light_logo.png", + "dark_logo": "theme_dark_logo.png" +} +html_css_files = [ + "css/custom.css", +] +html_output_encoding = 'utf-8' +html_show_sourcelink = False +html_sidebars = { + "**": [ + "sidebar/brand.html", + "sidebar/search.html", + "sidebar/scroll-start.html", + "sidebar/feedback.html", + "sidebar/navigation.html", + "sidebar/scroll-end.html", + ] +} diff --git a/lib/python/docs/guide/amqp-client.rst b/lib/python/docs/guide/amqp-client.rst new file mode 100644 index 0000000000..0bb970292c --- /dev/null +++ b/lib/python/docs/guide/amqp-client.rst @@ -0,0 +1,9 @@ +AMQP Client +=========== + +.. warning:: + This documentation is a work in progress. + +.. automodule:: dbrepo.AmqpClient + :members: + :no-index: diff --git a/lib/python/docs/guide/rest-client.rst b/lib/python/docs/guide/rest-client.rst new file mode 100644 index 0000000000..4af546168d --- /dev/null +++ b/lib/python/docs/guide/rest-client.rst @@ -0,0 +1,9 @@ +REST Client +=========== + +.. warning:: + This documentation is a work in progress. + +.. automodule:: dbrepo.RestClient + :members: + :no-index: diff --git a/lib/python/docs/index.rst b/lib/python/docs/index.rst new file mode 100644 index 0000000000..e084d4c3b2 --- /dev/null +++ b/lib/python/docs/index.rst @@ -0,0 +1,36 @@ +DBRepo Python Library +===================== + +.. 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 + +.. image:: https://img.shields.io/pypi/dm/dbrepo + :alt: PyPI downloads per month + :target: https://pypi.org/project/dbrepo/__APPVERSION__/ + +.. warning:: + This documentation is a work in progress. + +REST Client +----------- + +.. toctree:: + :maxdepth: 2 + + guide/rest-client + +AMQP Client +----------- + +.. toctree:: + :maxdepth: 2 + + guide/amqp-client + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/lib/python/docs/make.bat b/lib/python/docs/make.bat new file mode 100644 index 0000000000..32bb24529f --- /dev/null +++ b/lib/python/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/lib/python/docs/source/dbrepo.api.rst b/lib/python/docs/source/dbrepo.api.rst new file mode 100644 index 0000000000..07eb0f4078 --- /dev/null +++ b/lib/python/docs/source/dbrepo.api.rst @@ -0,0 +1,29 @@ +dbrepo.api package +================== + +Submodules +---------- + +dbrepo.api.dto module +--------------------- + +.. automodule:: dbrepo.api.dto + :members: + :undoc-members: + :show-inheritance: + +dbrepo.api.exceptions module +---------------------------- + +.. automodule:: dbrepo.api.exceptions + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: dbrepo.api + :members: + :undoc-members: + :show-inheritance: diff --git a/lib/python/docs/source/dbrepo.rst b/lib/python/docs/source/dbrepo.rst new file mode 100644 index 0000000000..e8febe7083 --- /dev/null +++ b/lib/python/docs/source/dbrepo.rst @@ -0,0 +1,29 @@ +dbrepo package +============== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + dbrepo.api + +Submodules +---------- + +dbrepo.RestClient module +------------------------ + +.. automodule:: dbrepo.RestClient + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: dbrepo + :members: + :undoc-members: + :show-inheritance: diff --git a/lib/python/docs/source/modules.rst b/lib/python/docs/source/modules.rst new file mode 100644 index 0000000000..ebd6fa8799 --- /dev/null +++ b/lib/python/docs/source/modules.rst @@ -0,0 +1,7 @@ +dbrepo +====== + +.. toctree:: + :maxdepth: 4 + + dbrepo diff --git a/lib/python/docs/static/css/custom.css b/lib/python/docs/static/css/custom.css new file mode 100644 index 0000000000..a865897410 --- /dev/null +++ b/lib/python/docs/static/css/custom.css @@ -0,0 +1,105 @@ +/* Prevents two-dimensional scrolling and content loss. */ +h1, code, li { + overflow-wrap: break-word; +} +/* Provides padding to push down the "breadcrumb" navigation in nested pages. */ +.content{ + padding: 1em 3em; +} +/* Improves spacing around custom sidebar section*/ +.sidebar-div{ + margin: var(--sidebar-caption-space-above) 0 0 0; + padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal); +} +/* Custom sidebar heading text. Example: Feedback Section heading. */ +.sidebar-heading{ + color: var(--color-sidebar-caption-text); + font-size: var(--font-size--normal); + font-weight: 700; +} +/* Improves text used in custom sidebar section. Example: Feedback section content.*/ +.sidebar-text{ + color: var(--color-sidebar-caption-text); + font-size: var(--sidebar-item-font-size); + line-height: 1.4; +} +/* Removes empty space above the sidebar-tree (under "Feedback" section) */ +.sidebar-tree{ + margin-top: 0px; +} +/* Adds padding around AWS Logo in the left sidebar. */ +.sidebar-logo{ + padding: 5% 15%; +} +/* Hides a div by default. */ +.show-div-sm { + display: none; +} +/* Hides a div by default. */ +.show-div-md { + display: none; +} +/* Positions items starting from the right. */ +.justify-content-right { + justify-content: right; +} +/* Positions items starting from the left. */ +.justify-content-left { + justify-content: left; +} +/* Hides the sidebar and prevents keyboard users from focusing on elements. */ +.hide-sidebar { + visibility: hidden; +} +/* Hides the icon by default and applies relevant styling. */ +.nav-close-icon{ + color: var(--color-foreground-secondary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} +/* Safeguard for ensuring menus are always visible on larger screens. */ +@media (min-width: 82em) { + .hide-sidebar { + visibility: visible; + } +} +/* We only want to show the close-icon on medium-small screens. */ +@media (max-width: 67em) { + /* Displays a div on a small screen. */ + .show-div-sm { + display: flex; + } +} +@media (max-width: 82em) { + /* Displays a div on a medium screen. */ + .show-div-md { + display: flex; + } +} +/* Apply furo styled admonition titles for <h3>. */ +h3.admonition-title { + position: relative; + margin: 0 -0.5rem 0.5rem; + padding-left: 2.5rem; + padding-right: .5rem; + padding-top: .4rem; + padding-bottom: .4rem; + font-weight: 700; + font-size: 1.5em; + line-height: 1.25; + border-radius: unset; + background-color: var(--color-admonition-title-background); +} +/* Apply furo styled admonition icons before <h3>. */ +h3.admonition-title::before { + content: ""; + position: absolute; + left: 0.5rem; + width: 1.5rem; + height: 1.5rem; + background-color: var(--color-admonition-title); + mask-image: var(--icon-admonition-default); + mask-repeat: no-repeat; +} \ No newline at end of file diff --git a/lib/python/docs/static/theme_dark_logo.png b/lib/python/docs/static/theme_dark_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3d21cd14e55afc972f3903b2cbdf4f3b3c8cebf6 GIT binary patch literal 19636 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 literal 0 HcmV?d00001 diff --git a/lib/python/docs/static/theme_light_logo.png b/lib/python/docs/static/theme_light_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..81b5910e06b6046004fbeceaaff25a483ff6870a GIT binary patch literal 22600 zcmeAS@N?(olHy`uVBq!ia0y~yV3`NP9Bd2>4DG^CUNJB*a29w(7BevDDT6R$#Zvn+ z1_lKNPZ!6KiaBrY+ULyqKJ&rH>5U7Ud8IEkW~ST_*uHSK#G`|^J$fZ7HeSE6flvBJ z!R?MrpEsrpC+1Cb&d8Z?j^nP1fpqX$*)R_Q$*|1_ITuQ;+$bnimTcyJu<1$#1E)~Z zkzT&@6>iH^{>`+1Z~Xb+&l-k9J8#F%{?#j!weRzvdF9U@9&Ts7YPNZgUATyA&0LMH zq{Q>)yDLtZ%l-eoe9smJ28Js~wmR=SyW2G|=k3q>{BQquef@B_{9RS!YVV3w9kzv; z8^2%Q8JA*p@7rcZ28IUZJ33uS+r^jvsux|V<xnxJY39?ft#Ki*>gQ(PRb*gbaM)M9 zmt*Trb+@-x-%kq8Q(GWDOYD2i%p@iThK83rcB;F?iY+SQ^yjR){!GbWu6~Oe0|Uc~ zO?4f+Wa>+8*T)<R_N)r_eO?{ET>4`-NYA&O*SZ6iE%Z{b>0Ky3ds@}Tlfq9J7#J2r zZtGrjLtvqsx(#n;TIF9CeKXyeIUo(!e(VrmV7BK;v+z7Nlg)ef-RSle&-%#A%)r1P z`R?nwM{8~We3+}NUSsj_r9kzv*<mqDK~nPselIZdag_C`xaIzSWr_CQ7f(7%7#J81 zBtAM>k-eAW>B@>~4*%*eO_x`_28(mN`^t1>7Q<KBhn@vY3=9mXR2{x<v9jUx1BJn& zYwRn++LqtD^uhEXCj$e+jE3sHKepW3b~rD#Vpe2a{99eV?ItoHi6s$E`#ya*T&}nM z>6K{Vd1=$GpZ~sWj$i15+l&kh47XlK={w|_{$-JppI22q$E<6SMR;RAJIMd<n{U|O zee&1ee#L?B6PA^)&GRk+IYaA3`bFKU?OUW)P89YxKK+_|ujca0qCd(Q7#JA(J{1Ov zc>B(-%er$UIRDg3{YAnI3=9Fv`J%2HZ*O_I{Fsl;*D}jku{jJ33=Ok(--x!0`~7mc zn~%*|o%Q-(%Pc#=e!Cm{;%!~jir@N6u7Pb(?^+b`danY=0^iT?4)foST>X0e%54u% zG8KZ{shYLBV$Zbjw^iNS%v{w$BCJUtf27H-zjsy7=Igwu@5dKu^o2vbJEP_D-X9^W zm*2bkY*lRj{L60ZKY>DF%hhUr`<nEveEYvewJeV-DcZ`%z`*dpT3o*F$GM=Z=NEl? zxU#&y@bXnFki(DXh`Vl#c)IlQ)O#OQtU&r@R_y=&(j~Ni#ioauX_dCej{Z}Eh-*B$ z92PpiBK+j)vb`_91hVcE1sPzq<NEHh?V(HGZ!Hq`2M5uC3w__$-3`t9v@+<SCpefT zBA-4APCYdF1q%ZMgTXehqszTo)GvafdV$H*PXAzjh(?E(T7vVJ{R-KAPye5O@NZCA z!7wX#_xCNkcVGSy>X-XyrTAR4<zLT(Vl<=i^4^ong)jeD>Zb|{@$+97UH+K)=QGF$ zQ7VrvKh6A8q%JqFs`c$Ah^>at)23ZFSF62qc}B&p7gNnZF`)MR(3wxaPO1EF6y^m* zf3|Dj9@CTcpiIWl|0gZ2^6#G|eaT7wTW3ec#cOpvTHN{PFi4-piM{tGzuUISKSSwZ z=6T^aZ%RRlX~w>{aW%VU|D3xde(Po*P<mZZl=S~w)Hid}PFeqrK7X#T{IdjU=XvLp zZyGe~%7-hrr{vAF0A&HwwO_+jmU`U&n6e&}${ci;ysbVpr{b2tT#yDW{cpBw(`qJb zf?}k0?RQ^L9t^0p2RTdU_imlPt76~J{!_fNJ{Y8W(_Njeq=jd+%a{6rLT3Jp*o`3d z3*!8y);?YLW_6kN-h3fY)cw-@dMY;a;mYqKyW^*Q{SH!;pyjpuY}m6Sm!mrC^+AR$ zdwk{0rz*|xcV3ZtcANL?3lTZ$9&8CJ*cpue+&#>H|EBEo_txKz1m`c@kqJt$4QZ0U zPsK`xzfJn`s^T!87$^<DJ=eQCqh`$qlg)eV#BZOw{Z9*Iw8@o-VE@N{Han9*5qcqJ zdDZT{?na-AKHgh#b^hODpdiiIGTSv!$L7z6xw{YlQ#wAgLhI?qDv;igTPqi7gt`4> zbDvdV^cIw?E@*x4S~Nqt>{aHKPW8*6yvOjiux852<qOs4`B;OJ#g<DKg8di!*vu>g z8$EyF@_B7s{(Cc*7uO|2<S!ZQf03v@ukF`tP*_~hSe`cRI{Q0k=Jw??pMKrBNJH<( zUXa!cS<LGbrP3Z{zBv07<QS84EALq?Q|1)`6-fR&(m>vEIPbvh)$C_8_kFG&h~Kci zDtjiMX+}lSSx|Ot*xR>9wmbz~ylHej+LFx5z`&5u|MBo-a3C^#6ZAfIUcStrxn;TY ze{e?Fa&6(=3{dsN{-1yU>ahAR)nH?p|2%J(-&b6||Nq56VSo2~S3xdcaFuUPm6ZWF z_B-4kZ~Y6Z0~jo>e0X>=4-`iXY#+k%8(wO*w=Q>8=LVJA4BuoEj6Q<G@xTr?e*eA8 zZ_1W~HSr|epIyxEYqJyVs{;%AzO&7gD-M8pBhkk6#Fr3|H|A@1J(>dX28VmQ`t>)U zkdZj}a^};o!qxxJw}9<E^(i7;XdXyL<MhFYC+on1#|mB?fqL8R>y3HK)em28f*8cp z|7+3*kfR)C%H3U2ZDcdI_zXyM&%>9U%V$0<g+y~t_Wjjm#eLw?$Z+nf|MTDT`m2F0 zk+^;4Q&su-$q+Mc?lONJvlSdZIsUE7FZW4+GJ#M{{nfm*%DY=Y`Ho@fD}NhKe~=wh zp2jHJfRe=o*9uUiFDNlS{kq$}U=2tz<wg5)*X5v)bLf2L15xG(R;J)?Z!@=eA4t)Z z7cHG&4;txkfQ?)t0CtrJ)Z9nxpc<zSltUUe9X)cHb2%tmIoyP}QK1#$>aKK|*H7lm zwvWvD_vh@kKShig6VJ}QeYF4n{!3l)ALiHpbK7X~KZ$jpu-aPR{pS;d_P8f~{OjM9 zZCxH!&*z)=2vo{8h#Ug@P7@LfL0FyR1aUvqTCRsfMB{;wJAYqq=aZB21GzszYg>M) zjE%`0NZ<{$FC_gSL7ifu3rQqHG>{n_20A2ZpL7pgc~kbzx+cQ_l=2%^3eH<_lP1~a zk>qhJ%j;$DM2t_r&i(lmRJQ1*5BqG2D+Sp+j#wz`ch)Zc%-MYpuI9~LyWZ;P-u!*J zzYQ4}7=o2-9KChsRxXXG{cNgVep+YWf6v0)=YF^Q85tO62!+1|6|=D7bZBR-hLtz} z{%Snj&L#}1BWBFHd#<*fpI2?P)Br^>?K3~F<OwOh(JF3grZlz<zrQjcXJK##HH#7$ zcF+CY$H~y9LXCJ&$SSk`E^B8p15_6txWeIoA!qnD0|qPt8HQuys)12wkIm~D3<(~f z6vOl2NbzwQVFn&gP!o$`S<>s(ea-n6pn8&FMpyOyn%57cA+4`0vFGt6KQ1JIbsd<U z%pfrjl=z^L!&dSq(tlg?Cf$8Smu5OOgWB27H~#)=e4Mqhb$a^kIu-^4k%ycN2^^qY zz`ekwc(cBRAEZ8JOF3vU#lIqJ{n@#<bM2eLz%A(+UBR2rTUTU&UBZ^aX*A{Z7Gab5 zA6OU+T6W+6bGC5njn4G{!MFPvyiybGp%QD?TlK9zy2rDvXl6y0xqnwKJIEhx1;123 za-LFpwr#fh(QVBjM>`uVWPBP~@v_}d7~Hm$X!1`tT*&y;;@_K%kB$@{pOe;obM_6s zL&6LP1oFz>EY7g@-_~5zvEv3rkb&pmRRxXXkN$M~2}L<nFfm9>dR_KbEWFw8tlj-@ z$qgH4$~}1(zd!e{<d#N|i&Y+SPL?vTyWE^}zv${ZUQp9RV$uhe$CiIDtvUMWf)L1_ z2|CV4gHj~=v&()&-jV~0yjh}fXtJ%pfmLwxoxjiW*V;F41BFf76aLd8_bd)a{P}(O z-F5z`dbV6puY>30R|c;9If*IXtOF|o+*gB&H|L_OeFb%i=PVC!{MqfCShXXxMV+BR zvGR|xqMBJ!5wk?!2N$R)gTzGh3kGu@pO^a_;;}1cYc@Y9x>#mgZ<m!mB+PSizwwv5 zZzA;WiGiHP@upJg$dh9$Z$6*P%HvV7$l&c2D~R~A1?QzRC46Ewe}2#1`T;8XXW`6! zOO4lCa7#42aGB4ae+$&@l4$(k5+AeubBVRZtxGp6m%B`#zwoV+4I@L(f#qA@Jmoc& z(JNc1dv5=WDK9=5Z;1stT=35CV?~Ago^uJxoZGMbCiT%uh;Z(^J7py$XV2~n*i(8- zI@{vJsn_eRq~&(rtbAY)<pPSIi|?-gdvNA^%=YHLb7$M%?A;vj<HEA3U^91W9f&KO zs`2dxL;vR|?%zHw^!*JAI^|C*b^SlFNy~{FUNcds&b|BVR^^r+P;4ssKYi%=?Z$z} zM<V7uNl%BU?oz*f_wVZEw?FO`J};}SH^=ZU<HH}Dj~|Jcr)0CWAEb>foUP*0tJ&)< zg<rh2?UvxK-6(Bnw`C#N2ZlLymB&ARoKaDAJ$m_b8LqYJhd!xX_InGeD|!x2etYBm zDf>Ssc3#LTd}^Yk^G4NXE2!UVz*gUpX5al;U+noae=F&z(_4gpe>AyK2C8n3ZK(CG zNUFbAaXD<+T#F9=>|3A=E-~4^;Aa0y^;=#gsrF*s+oyvZrpA5DvcvqrmflQtzDMb) z)u6naTvXc7eB?^-9>Ml)0@0QyuB}}^*Vh{&bG7eqg|c7tpHIEleXI)=g#F*Df?`dx zEbPcTt{eV)1pCdOTu3=S-!^Q`o`0*Zlo_w{zUV;7SS2{Slw)=WB`_=vZ1k74_= zsOsN2g^w)vAHO+2yX-BfJv8M&sWqQ){)xxEmL?ZpPEYGzy18s-p@IClEwewcFa$7h zAG7q@V`y()f64NqMfU%{J>pZHe-<pe3yMggBk#IyBr>)ees#J0=gn97()sr|+IMbC zezfu|NP5x_m+6aj_Sj@!wCEMAJ{@-dN4362`>s7(UpGX73OJ3<4@VS|F8%<eY0<6Q zH*U#TXyN{8v*1Hch6O6qmK$|cESAf^vGCkIE8Fln@iRV46{*-u3(jL;@Di$?2da$K z-Mi*mY?NkyrLpct=W-sUGWlcY-ER3-FfnLM^e>YPd2_7u_yy;?JCcqscvHRzl#x%R zPG@Z{dfI2z<!KxKMq<kRPEcRoVQ0;*(*}<;%lB9Q?lE30mgICk_>Dx{2NzH#pYr;i z`F*kI_?zhmA3V}5-?&0@)tdd+Pl{TziQ04Lm&sY^gZ#Q_W>r$e$F_f0-h4jSdkPfH z<xc!;Ptz4^K~Zq>PvY#vqar7t*8g08uGec<LE63uE9>`16E*6@E~(ovGEC8tla=kC z(3W__<fOZ}j`Z@Vr1il*)&~vBtiSKJ=UxbMd(n?2(*uu$c=}9t5IqvN{Ph3N@^4F% zGG@o$E6lI~<zf|qc@m*-s;$iHFa0zuoo!#gdB*J8gWLx%{NYK_f5^$8aPau;e;3Za zNV*>!Q`nT*nLU46r*YSgtas@O;1uXt^XT-7M_Iu>c1J_iXKabz_i<M0qn<P8?dE|p zh04eF{9}Tb)e5hA)+9|zI(`F`v5MWErOW$+QvakMhs_^%{ScY#Z@?)y_fK82%(|N! z<w3Td+?&5obiTyxpRGIJ?UOwrVtv#_{n*oVx!<6u6#D6Gk=jvf{YW@c$?E7<KZ)t{ zE8n<RFfn*M;eR;AwMNO=-s-4RwM2V%nGC2{@p!UJHdSc8#^e_+Gykl4tN;pKqk4(u zK97V`1?FnpKGv&0SMxX6L9u%_Uf$cpkSaJ|qWy)-%`)rn-PzXVbF{&-Y6q?er@lVE z{PFoNcAj^KX8ufiUH^CT3zwCDdLA2q%2k#fO%81x{)Ri1><l|P{G`3ro@(5`b@lvQ zEq0LUY6|NVj)byk+&*;re(Y>3)8BhGKXm{lpBFA&pibA>bIk3h8$l_9^X86yk5_mK ze$0F=ZvXP#yT-jqQhMuB*6sPXs#vi#Q@sxqbv_-h%Tl`L+HTyfzAo7DVz-|muXwnT zqO*;~;X0@aj~Le*cXqzr;VF83<)bLkw_=y`ckloA{GEwRO6QA`_tO5kPYb{f-<fw@ z<;d1m_W%FzdLNH|mA?M}iOH{La9n>Q(E=)a9L4kBl&n{C*mm@%_-<i+P&xK`TE~SS z$LoI{PkQ@ybFMv?n!>cM{QIvgvid*~?=vIT-^a^m&yl|Qpd?|Ozhg&~c(C7d@n^T{ zM0`3j)!9IWk<SeKHO85%^Y?}Qc`E)tWX`PGdpFGkT8?)36>rwx@>gSqfcH_!Hjo1n zr}Y@~ZcM2H2gm<~l1eIW0X-WyCW}noJ*~@F15^icUHkW5yl-x2_v-SE|JB9%XV<OT z{*LKm{GTJiuV-*g-p%7&!Nf45segNU_uX@~?&m{}JSx2G|5ar7oTO8lOpBIZGvqBi zs;5?-7yt^9DL;<P$b98*?|S6Jlijbn)vx`1_SX1rtlzRZGt}&NOH2bfN>%!L`7Z9L z#92y7M<&0w+qL0FuXM=yR^!h{->v_@Gotcu=z6|j(aF26m%XbHl;{F$**N2BaN~-u zrJL^`?X7T{#SqGC*eUjVhuJi-*wu@L!t8&Z;XZf)lms-R_I_IVc)5{M-VxzTkF3Id zZW_pL{9)S^?6c^oo!*M38D+aFl8<LY6?d_&EquziDkbk*>$@pu|Ns8_UtocnMpJN1 zWzR|DgPp5BZte;MRSUt}3LksxGfq`;@I7|#fk?RA!mK3ug-ajxc!2_EQ^b{BJBx3h zc1V588(?4k*S*YP>XQA3Jqs4SQnET4=Oh6dnG9eQ+ZMzwxZv77yW6~~h6{x!Tk)tb ziuLJtJ+k?5r<K9mBRl35HZd?<zCOR|liC!k4%H`y3x(r9{x&PyA7NFq<Ffu)G41vH zqd=u|i&d8DynR`>ItxK{xu{_Ny;k*CYIWz2h5I-imA*2g=f=LPAFaNXnY(60S$+1} z_~BCy>po?PdCS)u>48dorKBVkxm5Y<GZ*B2^K(3!d|lbe)h9Q)<<;?JKZGW&-2J26 zV%|Qk$Doo>OQp>As8ITf%^k}N*Xry`viEUW@uMrd(s$1$;p;DcZ9JJG?3;9}(+KR4 zq*tBGZd!ASD%s`M{k-e9dF^_utvrE(v&7;pf4-QtaIUY(#7y;5piCIjvr>K58u{$5 zM_b>mH+mFYbM)(;Rd$<f*PUOx@8kEonQFqmTUwFS-b%?o)2X?l;!yI{Bm2AmcIsv{ z|6n+>ML6|m<75_w74w$%`mX0<5Eie#ne<BlVncvW?U66+3_d}j>-Pl3g9<O#qoM2! zGej=?rGR~Q>WVPK<Sj)ncV()3fhvZO8>jZ)|L}=ng5EqGN&f8(n?Nxyx~+C~a8As3 zH-?57yB7-GA6OUy3Rj2Qy?u9=p@FAx?eE8LW7EAsV~h(P<&|Aq+wlOl7C<iI8<&IB zWxo_q8J4ARX!1&RhErnKcRr3XPXQIjSqk%vMEs?h8a5qam#W(~0aTZ+=&ZOE@a7>y zgV7O_+?oxHGeJpOD)R3WV|{Uk<lArW=-=D@D4pvkSa|2Q;^n*hnHi34+_LfIpBWsW zm`zKPZ+Csn(DUlkwY=}mJTq9Jg2!J?_1~Mrz@uxo!{F@^6_C$NzZ|@(uOr2tkn!<g zYwIH)(0oNeu=fAD!`|!%4m@J+{_%iG4b-e!vGQ%Stp85tgao?{2~uFIqj&$R{66sp zL&1z63zY0OG<br>ezJ7Fl|%?}A4uG`@0G~=SVP6rC7@JcD&Bvj@1#1zjq|6LzCPEw z66AL&g>?Z(ls7RZtpAaqWVfIp6D)XOsx`wrk?=P*Elrsq1En54etS3R@vpw=!VT+w zI4If8XvhQwrj*LJii%UR4D*uQLqRt1K7w$jf;o>h%NgE^^3VFf$Px-lY9;|O8$0~> zFlJ1@sy%;KlKWDyswbyp9e&5$yQOQl<5z3=JTI^VwCq0ir9NgbnXg?IGW7!s!-a&8 zC(CP?_#8IR{eQQ<WO?kuERgrxy8RD4o?O5%OY_^Wqs^)dx=;kui=UT$yf^jyo=qX3 z;_d>|N6(5HB|eAZBPOqZ-v4(}+iNK(_;m&6cN|xL%wW>I*Y66bA9X?L<H;X+&-YE2 zS@7)9%BA0|0@*={w!7rJ?apb80ofn7T&l>@geWz-z1`}r`d!8V`{?+e?+admdK?)G zyUokGdS6?|RW(R;rals!`=)e}AINFbe!V<;_MQ53hAYl4%V$KjT7f(pmGkkh|E~E9 zm;-V*`uutPHg>(xR%IJThAj~>*9GP?tkPZQ7OmzN3wEmbb+bp4OBhzEuX8zmHDiVL zLr#W<cbh(5^|vb)U=ZDOd*}VRUaeN3D0@-${#5$B67~SWLe^h@^8Y{KEV}~gNFPux ze0py3JBDTN|GfWqSolo_C}B3Z|2lo_sCvW8$Xhpb*1f&+k?R5nsQ=yH`9mbX%H}0s z!l$JBac}N6F9YS*3a^;=9VgX4awZfdosWBSyIBq7jT_-dmd~hRQiyk1K0ofu{hcPD z-pGMQyL<OU=QB(?8n?G%2eZk<4=fA^7A9Tq_`xzkc~NjoZr|Uk3oan5+`E2kDf?Q? z&KjU}vC|A>jl_xF)ALHoComY;UF-z+NF^e4_KD7CI411R)2n@Ek#Mep4I_hr$VbkE z62ZAimo(?ea6yDjK5`~hEE3KI)#L_3A2|~W776EqDr^IzkDL!O776EqdI1JvA2}Z+ zEE3KIl{p4xA2}anE)oW%$tf;1Oh4}BJ>U1j<#lWN93}>a)XpCw)6Si<4qmWz+rC#N zi!blZ-}iNP!!_fNEDR1See4)>)vOLa26ec-#6M<cCSN|tv{uRL;AN0VP|Ti<%65!v z1t-s2By4lGPo06`l1mL&cK&=_Kb!J1j?BFx7gz3o%vUi7G}Ra6TGOT3P_f3s;qscg zPcvVzw1dLJL%d(O|A$DwID1Cd4!=L3L_MznRL(SXcKq1#A;12G-hI{;n`b=enQ?Ew zACHVr1rtL;Qqs%3J(0WjY<+#9F{<n2*&pvWW?Pr<X)s^_C7aH!A7Ad*eP5pR_SJJf zxfN4q+~_%ZcE$r1Hc;9#5PcuF>*C#hrU3SjPj<~H>8jx40rg%IvXbn*eE2IK$p)lq zPhVF)KStf^;0KT|m#x}e@+W-%_7a&Db2=V9um8RLQS<8fioyhN>;{)_{=A^NRdwAa zh7iXiAAY=F+P~eFhw&JwuMxmm2$DE5RjeViOFiGMWRr``F;J8QXcjJ2z4zhfZx$ct zBkwCdEvxkl-~&1R6sW+R@%#G!-1ou;hJw45t8PDMc?v3!RxFue_CtUFFXxSAUyJnx zCS(;qFEcN5|H5_(6#D_0AYaw4nzFjwq3x=_op#-yf1=^96*@n#FgQFt8oX4j?(6S& zAxsXEzrtVN+x=<g=N7Q*w-#*vyuf;@z-3N`#~sVB?Tlid40gD-;aq`WuHAEg$5|w3 z7(RWnveY0Fl$N&&2Xk@%|JDvFF~#{MJZw}i2ft4E1PZ1s&1Ir>RfU%y6+ZANbTywh zX=m-~#~Tt3f#r0?`j4-hcH<_~hHxLtqa{mN9~sz!%@e=uzj5=?FTxkLmwo;G?y6O= zpFu20qBrpu=Zp76Ro^f1avxd91TsYG=ojHF;q}vlZ5kL>uz^C$?UlcM<tJH_`_Inh z?w+$?5m?pg|4(-BDBFB{=lj;Sw$7Eurg~rRmfv50R8M^cs4?1btUKGh{`>m>%d2^h zf3?nNn~~PD@)-ZRx`(2zT%h75qjyK!<`1jyN89n4ie2tDJSuUyXIi`SSKG&?ObiQr z^UCB(ruWYWO*!l6iWP6xulT!=Eh(z->#5~xa~7n5rn@hAt-J47yCaPKD=2C%WEK8B zclrH-S-<>O7q8`e<z2zVu;A-2|NY$zqWL9_FJHYc{Itbtg}wDW28IRd-|UY{-@WmF z%dcnIM<4y^?qgHaShsgw%&xeYJ&8<$vp|FE4eCe!E$0XjKX;~_c?#>33EEAUZ(TjV zJAU02$IYNTUeGXOSBE|$R}qKbg&I&;$~ZPOeL5<v%Br4W2O7>}Q*k(0;bY+l9(OTN zVdQF4wr~V@84Z*ex%kvA7(u#vR?05`b=BAWIo#iM^ryJ2=GtwycizAIu%t3Oyr5|s zD6AH!$mhQ=+kfute4T0M_5|t7om;+T%c*DQ-X=Z%Rjw$u0F=QO)LbunSF%5Bea$W2 z0C4NF;@ynN%Qh;R<=*+R>GyBB1W;#|VbYy9HX8SKe~SFB{msF(A*!PCTH$4m!ny0@ z=REpub)XIuw=)|4$*s7of3}QyN_NGSsjo{87=VQryp5jwXt}s~_f+reyFY5Kdo;6Y z(W^%<OAK#{_%AoU%fbWdUo>cTsMppOeC>W0UeERF&!IW70U=&CQ}ve3PRg`9paC{$ z(?5|b{`z8fV*>;iJmOxK_<FTp`7X^xlV7SO#DKyi#G_vP%E$En(#MlqpZGZGaJ~08 zcjaK!05#bdL`A|s{t|q3<;~~29I5gmsY0_GJi!6*=+Um=dig0FM=g%1C0qhov|{33 z!BzFL=lwMJgso>QnH`;egT-eSXk68F>vyZTiq~IaHvTwuGOuOU?An8;Zv~bA|CPIO zXYp?7S&!C>wJ=`>mHrOSN0i?`etp;5TrT!x^ql&Qze^wOOpbfT_S)anHDlMd;^l9T zGlq144CUSO-7C)G@1-}3Ggoi~UWsm5*5NZd{@%-frpxX#tn#Q}VrUTSxNG<EYf6Wa zU?{)f^j#Z0Gnlg6z!FtPE=RBK;RyJ+WX}&HQB&KGPj>!u3I22Ss^x-nE$SJaKbU>~ zJZvrNe7cCipkv3Z?{^n{djEfVveqj3MMqZ12u_>F<GXBTgPP#H1*+fdKC&?+2>e~S zVy~f=+Tur@m4XW&tuMB(iJKthaAfH__Qv>om0#xj8ZqXA>OThN?vpEiR2j{Zss9#H zuC+>faa#O~f6s47Z~btIKYFwMD)}i%S(?lGcGv%Xmd_-7Aqx~m2HSSXi8Vfr+E|~` znO!Zo;8&v7m&YOB?e~8<?6sfc?}{t@Uas$F#k4H_@xNb!{lMk4u5!B=ovugc&w&;a zH5mOpkO`V}o&a620_s*VKu755J(<z)|Ngzd%_nM8tC{|1O77{|xY1$qnFj~EOgej6 z$`*OPar~epSv{%HVwOwO^d0Rd%?fAtw&$w;o}gqI8F9F&MU=NgU9~95(Y(y4E9%6W z$+;6YNH$N7^DEzaHR%89SF55+-|d~x&-3}ikCpYM`Kwpe-M?@7oQZ)!L=ilZv4G<s zcv*}q2WZZfLBk1@k{BEWo4~7fx&*+JZ~-k3z$;;tSioy|MzxLx(`b^QTh6IVj4ZBt zJ6)gmd0n;E-%`7u*ZmLQ+AhtxPGx@i`9D>P*SGwtNUr$x*ng*ZI^&<YNin8>*4?T8 z5!<wS0b6>!z3H5qpQ_bI8~4oLp#1px{QKu`Ry@9<efVYY`el3V?$_T|ukg}pYTjL6 zy}v8lw(x0H)9Mqdc00tk)_c#tFTd{IPqp`tbWT*K*BRgW^S$1?ey(}=^>+Jz$L)n; zJoi8HF8}sD{{8Nr?5B%AN58Kx5O}Zt=ERSm-yi<}bG#rs*Y@DlD{n9Tyw7|3{r<V% zOz+hGsBgFb^S7)?d!fkMvd*Kt8N6blHz(h0f9|LM=GCL-*1h+B$I9+szV**ub_R{N z$CiHICijA=RM<vU>|{Q#jpyC$i3fvi)_!8#8?Fmer5n|19R7;ov7n7=+4uRSUreqC zc^dh5^I3{X)})4<u37%I_kFv&w7tp>A%+H-X?lB>9%j-~ukg}gTXS^5);A$Gw)%Dw z;<hKQ&a+uR?dyd!c7_epr&+I<{Xo;Qc;2hY3vYQGz5F?A<*C(6PYE(4%zNZwrduAM ze=xY{nax%8!%;3rLuD48HTyi5f#H>yZu!3ZPIFoOPlgKH%>2CNX>aiQB{Nl*yvmH$ z{X92JKA*?G{>=R-ZiWpS7mL`hxPO=u!z0z#c=+bZid)k%Wm)`hpDAZz$Z)pt`SWDa zH0u?;4^}cgv$?7qd_!3NOq)TN$$ih+vJ3|%CS996&3Z-mgOyBMd1S)APRZvDvAz1a zy7K+PgO3;(9!LCqv8S}Di({TvfVZj(Z*hs&uLo~lu3dO<<`+4et40t*empw(=(za3 zElB~K2ZLFY9u)A%om=<4Yk7}u?A`aBm)|fj%-T}(aqimY+J-+Y{!>>hR9f_1`Hyj! z_|bV^Y^qn!`sp`y%Vj2p6v6w&bBowZ1#ChWP3!82%KMV8J8#<@yRTWNvS07KyRNNk zxy9C^x3~X@_A)TsJW{^be!hKjyn^MyVAU*1n>*Rr;m-<|U$<SEx^HrpIzz+b=%1J0 z&wavkl5?I`kn`@?^@{G#uFF33?*IG#nKdKB7XQ<-Pt+?`dGMOA*n6(>)V*`pk`*G( z)~Fv8W;hV?Xm`aN1KuYp6{|dSbuIUvtz7lcdEIeKf76!S!l<W<d*y{<I2j!7E_&s8 zS{AH|N1@O0$jz@|vBAzOF5jLPVtn6ot1!a>AD8DxKmE>+_VMue5HeG0)x0Wh%d8b2 zy7(W>JJr8>=KGWl77Pc@=)_iJSA6%GzdXk|t!eqG8K0+k?>TpQdEDAZQPo+EYZl(h z4EV^xU{D<OPC@=)aB9}hz_+STR=h0zX7YSxTW#n5)Bg2;G!IT?VA!H8$CbUaIfli5 zYNz87!wD9<i`V~O)%|yc=9y0Zhvt5JCq3{wwR+)IzuB@34w4s(u0Qs?Jz?sDl}@H1 zwqFB2im2|{dUo@xg|Ajl+xKtw>erm>KpmKiMeI)$Dpm#XP8G5ZxqijMWpDez_ojP} zssI0#`Fe^M1H-9~ne*qzTq$Cg<d~-wa(2%0W6ph2tJ|wqRIEzfC(OW*dUTDtZuy3a zrsZ5m9Uf`jdMm|0FDqC5&x4%X4}(QN&jmFFiZ_0`Sjo-HtyZxrg!gTk?)l0tb@hvW z5sRmNKU$;Cz);rRK1n{l;_|EmOq}zyLY<GgoGzZ1l^2-}N*Gz7jJe%!ISWIA-8B7u zm(RXDH(}y~l|kR->b9FMu-F}T`)w9qzLG%B*6Vw_e)@@qXfrWvdGh46zU5OsF^>-+ zhQ;&59&e4`x24p}?QB%;cH{e;3=Myed_Mw8VYvy6EdHXal`66f9%L*GU3;|58k9pV z&YYTGxBshv<P80T!AG+)Ze|vq3+4`u`6w0j`@-79_%QkWoVi|{3<v&X?rc6PU=y12 z_IB@D$D=p5Udy}gwKX97chJ!q_1rQB25!;&Pwg#kze}{9a8dt_*WE?0US4`%EdN^m z^8~x!Mh9EZIPg5}Q&-jP;J+K4{dP<3Re>d$o0%)(WTtHT_4fZ}tphQxS1-T(|80NB zqQi4I=4pwGicGJYU+od+J=IkAtXo#*zcAg;b03*_G0f|{@RGlNf39o8&Mx+or{C|l z{qyy++q^#KqNul8;m)u9LTt7g`L9^|K=WC?D{DCSoHTZZ11i(>_Jp22?7d>{gOx@7 zW_35DBId+$hx!|;*g9>Qwk7Cm$l=$ZqHcosmOGYD`A!Mjm@a7Z>Jnq>yVh3N?)|)e z`U9<H;jTx6S(gVeF?><6Vp#N)Ps97el?JK2jA)MBsI1MmFFHx5@Ty)iv@!Qv$ikp7 zJ!5Bf>K~RA)rzbJOaIRola5}#;IJSKl$I`cOtW4fb1<0sk;Db9TW_`a@)oU9^V^>h z__lG@Pd^3*A7!~o^+kcFWv3`rWVwjGEfYOonYHn3p8MPNZ|}U`?^Ph$9{iDoVM6qo zs!K)esT}j97Cu_JChYbbr;wtnzZ9jf&)qAOTl@I;+RN&U3@37*F8*A`!6jg0`sm)_ z_uJSc(!*ZQS#IvPcT?$G-PK!Pfyz*Z%|iUM?e~3H_O(S)*v8c5YOthkxb@o=W)-Uv zH&n3ZZeU=@iQf0)+_mQPgfbR?-jz>FcHi1!b3fC4?bQswEzArF(~^3cgWn}=WAW!T zOxV*S*R*`$q3e-{YnER<E*-txkBK3mzhcMFgE#HoF($~d`13~oE=$gyP?)xL>)Nkh z8iS)hq<}(Zsms;iURg#^!Z$PxvE5s}Y~HHnQrcVkR&NUkm@Uh|ury<5^G!h;(_;-E zZ@yZ3Hcz5<N0=yA*oSJ7<-SY|0n2xYUnpX~ENo-Ctn0zbNm`36&OZ9GGF&+3<ew`- zTdo>_%X){uVRlyQpYknJsmMCBxL1A~n^bz3_t9YgtA5s5H)l&7@&={ouifkyPru)v ztDeZ!w4Ci~!Qp6U@9<pX{<Md8^8Ir|t5U;{)-30Gww95hV9QeO{7}<LOq&F4Ojoo? z-;-);meZcs=JieOx9PE$lL}9%Ffim6oRXE9|6rx#GaKLa&lQ_)GH;i9X1%hEi6O&# z%N^IJ!UmQHgPZ65`w{%>gJ$(nuk}~`oLQ@rL4ndOuzB7iMGXVVgTaflR_r)&M?LoR z$!b*>)4*q4>$RR)bIxO6m?ffHejtU#zjxK5yLy=o0{(`Jrff@}*}VPD!QeSslFPlB z7%t4S=y^W5+iz#HC&xUg()SO`4^OWDJLT8WqqEE0Y{RYJp1742akNJLsviS`Z}+s_ zQhrTsIxPOY#?N0?RLy(8MOkZ~QQeF%!}})_3K<%nW$tYL$T?4Hnr%hbsi`@Ke?IEC zB9+E**7+F2IZ#<=)bV~PsH}@SAi?6#yL#a#rD)df3omE(WKXQES1RcWR%K+!xc}*B z`Ib8eK`NqumnBEs|6%u)`;3@M#^!3Z+fgBgkmxdhw0q?=Yl~SARu<mOZ`sW1&(>}K zzIk)yq2+51zbL<)#?EkI`i}W0zs!|160$KpH)r|Bqoqdza_5~CIg*;cxjR_pv56-G z!!Mywe?x7t8}lEmEHn*CNZ~Z!a`WxfOy5o6i$SsQV)8U=i#ZQg7WSLfNrcJgFR7at zsTIlMU+u~o(bly^;?|74Qof*>i}gFEUnpY#Eo@V&@SC@=?SZCOa`p_xEgX_sy;&By zAD+FcYFf>}kPrw-W8V}jUO9YYd@T9QW@VXGMBCPrayCa_P7I9YU~t&K$f^G4-|c_d z4sf&h%Njh-?3o|$=z4b1ky|>yPWt}(>38yQECa){<d=t{5ARN&Glh3I=RB(qUyjee z)b9U0?*CP%`h8u?59=Du02lSMES~bsQ?GdC@~x}KHEfH^?D#Fe3pcp36@wZI0h22- zq^9~GFgqBmz9~HA$n4^fZ8}X^%NOe!Nt}_hIl6LUU?>LzgLTri;I_*P6`2nPtG~1T zlUMO+{T=h;-CPnZ{_fog1%dK&UOu<Z+|107Ab+ulonzjvNq^ZY1rG*?M9%%aR$cIM zgbPE+(HiyUql^qU-0!5<6kXITSEzX9@#{e*<H5GeZ;q9}{PX_VKjZ8h^P{st?%J?F zV`uag55W^oAFfP%GgY#C#^)tx^J+dGNxQeaIE8=SmYa+W1tPlTU;I8?nYcCIyzOi6 z`{4Z#D%p9p=BXLKU!eE9Mt%8BMus!~-)3%Q@2u|Y*WWYq^P>bl7XP^?<Gz}lS9o#P zc)y(AKAZbo^KP9jt_L+J6Sgls|2|yWPB*^B{j}^|A)C@;bC#!X+II73d*(H-S(WP| zzh!N?v3!LRs48Amo&57#c<8z^n-ABD+}DH5m9sfpd@lG}g7UW)$Nsi+u1i_}<ABZY zUp0P=CZIZ3Ja+GA->vr#Kl=TaJ^k#ArS?tB&)MAikr+`sXSY|fT%&VKw4sjm;uU3G zlP_(WCU}1JtCOG%^KJ8&{rlIH#V&f4d5`_ufkl50@qJOBCy>G$+ZOoY%gyPj+I2?Z zm*22RZS<GC&G>kh-QK?oUYuW%v~O~kdc(@Of1cLgowf5s$(H)dvwzga|9f<B!=Hb* zukQUcciL`9_k{OM`-7Fvh60r@4@GCj|FCh|xaRZRMN)5H_TQfBC+Lv6=iH6?9BqrL z)=hbnTV@!);K^(D7p^tS?)<&|`&+fwM}sH)_4{MLW;b{qZNBhh-JG9|zZ@U!TKrUR zm&^k8sJB*A_68~2*?3K#pT;{cujc3Z`TyUaSsSyodEuAOb3t|RhJxvNch;5(gY!jS z9>+W@5mD2Z{@b-BY*OT2Y`4zosy$`Zzq+Jg`rngNLA7AFxNf<2`Mz~U>Pc%{de=4; z$TThYOI?3M!!p9^^@Ue|Z*4CNcSsE~lIT&lzuBw~YR?pPMEZZU-Q{NZ@5mhMDus$y zA?HQ?BX7U?wtCS?DS=}FM}p)h-Msy#?EFI}hJ=lesw2<pmiz5o%-*!z?{4*v*jtZ( zwKo;dGnzkbTl@!+r-viwKbXSxVN=+Ww~)5Mo!fVI#D5EU6ZGhw%IAar2HZ`{`@Tke zowD_orqh8R2k!V^Td+7JHR#vDN4MTEFkFiK^{4I?`|I}3NdNTs_3umUB&I)Dnf$VO zInz}ANax<ehnB867#y<HmG!31RfGF47c>YkG#t%b_>?b7wc=I8FUH5Rp*FH&Olf)D zbH2)@t}nZk#?CO|`r55uv-Yn)dic7(p4HPnGtPNd(<<vO1SFih%${D{=66`4(d`=l zg-G_wn~<z}LU+}?th}AGOWhe$6e?cD_=oz>^xtg#YRZ;1jmOV4FMsoS?xg><j0`Vg zYu3l@`y0A-^@68;Vx04=u3w(Be9cvp$8Q{;O?ns^Iw^JjdlCP0i@XFF8t$yS_jB1T zudfvXzdyE`3&(Jt>3guU_~!iy>7p)2GzAMoGTk1xczt}L_V#6&G1nJxrVCq@w{!Q8 zzG>F8lqz1`S==k%cJ+AWYKt^cx0JPIU0eHYCEQ=XEdvz_2HGEGcVC;fd$RuiB~Sa7 zam@3mcmDfs=1b;IDgVvYf!7?v>>o2Qh(7|I3(z%th05%s!ZW`>THv9b({`WXzsWCr zzt>p4k7cre&BZ?rA7{u;F}<>*@c+Xb|MQ-`+LGD4_s!?IYAb!185H_6cCHQ$u|4}~ zN`8dMLE*Ff66d9?WYZ%`ukD>x)*EWg$PnOEb3g9iBgd)PtDp9Tam>3R6*Kqev8r1E zO4+mT7Dhc+TRCCY^33ZNe`}UM32kIxka=_YHb1DGS{<|S$2{Bai^>~3n$)+Y?B6)= z*;%vf6@_yiXwJKlVmmi^mRyObKLf**B~SUp+&>(d_T_EaaUQjE`|7f?f0Vjyzt(52 zWAl2#$H!r|prEfeU!*0#&`@=^`p4Z3MbBSrt+#)dKJ9+J^}(qJK61>vA(Ve4QaiIv zbJet{tj)8J{?GS3es}SgtA3!QI7?vjysX@v(YroOn`XUAwc=5_?zze=!wRdsr6HLg zRn)Z3s7<ZgH`&aZp+U_>wEC@Xd4TdkVf)i{&9<3`=I?CHzO^-P<txqNqroAJZl($h z47nZe%OdxC%d5(LsRK1szY5xX-26t@buybs<Z5uwzTd2F#;fxgspo=uK{+$SJH$3s z&bVmCjISkg^q3=54hq|M3tu++ZsQ;2J9+D*G_i$&*Bn%?)l3pLH){85e!$7Fz%%My z!af#%8};I=mv>&TH@&$3{-c`ZvVn%8Q-dXag?^;U>%?VCnlUi6ckf)e)h?x{eS6fq z6`c<{pQqG)w*C3ZJ9+xlWBbMb*O>(dicL-3e8FjV?6ZT9Hilo`=EB4<A^favdEu$M z(#zvK3sehC4ho+?8TZgIFW_R)*5FS2>}hJ8la0b{Qs-B)F&rp)r1<#L+x4qrj#ivl z_p~pHV_t>d7wh!ghf-=BTASGXH}e>8I`^dOve~hfKC{0}IaCR%k{8s5zpn{?`+eUI z^Ity>&E{Ok!lz!5)PE*_&tn$<z@k}&8KqM`y6OMaxq2rr9n{d1yke74^Yf8))H@IV z4@cx?M)w?>`FV=z1r5O$sdrazY+8Qg^V~(R^jH07V92;1cKWV3|Eij~h4ouLEx)h6 zL&##rgHGGD*r~?Z?yO?AXUcC0R!=QdND7&=_uJ{i?Yo!Hd%xr?AHxFe`S<E3R6RM7 z_~_sB=FOJx8xsU<3Mb6E{dN<Z)Y7Q!o8DbbWK8(=(~qs2nW1F)vuV2}OzN&Q$Afam z?^898Ya=#@G`dZhohJM+VCA*7TbAFJ-Q<7sxpnILU(5^%-yVIKH`O{meCH;=oyx`s zg@2#2iTJs}(%*LL&9jfs&pVb_J+Ts!&gzb?S>B`W-mko`KK;GU0{{IR-oH`5^lygj zl#Nvx60G8dz19CJ({sxdwN@^Yx4rFIXU&-C``g6gEMLO9N6+3J``a#7>$UZsinH6t zyH-)}-u-*~SL?g_s(I)Be*gb?flQE~P2qwSe@;X>iX1$8)%w28_w$b=F6KNuZSc!T zLOpd#$>Y=YeA@fZmd|qi^KXxY&CK^K2l{^W^fc~cn0crA$K9~gcQ3VWUv+!><>hg* z)9%+-|9Jg8UTufejb&H+m{apCZ|Ntz+ITi^p8aEsZ?`9(x#}1AbbEZO7z0CX=d|5D z%ia5xnT&PIQ<N$mIn0diQ##qtwft^X&gR>KqM?$f-zY2G+3XK0>L%RrI~}{0M@n7z zpfLL>skAp0h8d?KK8pBzE($4^%d)%+ZcAqgsLNje_18e|N`2ACw=CgIP3nCUD({_r z$MCp$^WSYZbswesCY3A9r~|bH7#PYrS1pf<ewTFZvg7i`!#14rEJB`1f4r+39eI2E zGf6hr)X-!6NAEHwKFeBO$<xTrut4u((e|3(tFy{|Ps>Vj%(Dp5EuOcDPi%g9!SurY ze*6AhIzQEK<#TJxvwRE<wMQ~<Z4Eni{LIxAHBGKUHiauZeuT?Ew|%Oum7!GAC7?B% zqcQ34GtKJVb3v8(l@-sXSx1;06n6Kzn^*qKeAQgBKgP!ee}j8_Te~Lmm~OvwP>FM% zMbIn8$NPVm-g=NJC~6XzICW!@x>wC~y**POczqKOt^K^tj~VRH={u!aHVN4jF0pVk zS)H-*^o@O@@*dq%A#=ADsb@P_WPlo{J{ODFecV1AiP)m(n>O*as>{qd_wyMTj&1z* z_Sl9$N13x(g#~O3#nkJ3pB?+#&i(slVCGVm3Zw6Gzy7i@B-qTqS7)#$zb1ccf=`qB zu|-{JH6Im4{GG%+S<nCYlgG?pps8DKV0TbBxl`fj$Ayni2FPwZ(Y5*($4dr=86hEO z*5CPgYZs_b({q;hSA|i?1($#of68rcc`!0q^v(GC#M)fO_rsAJEdGyA7I7_bc$yJ- zbo+~K9t;exzI-`8AJQG_Ijb{gacY$E?^2U&7KQ^Ri=Lh<_p+GtpmXEV%uFv&{^>8B z)xDpFrmgzHz%U~$#CB#oH@mQnq2Sx-_S0f16~BrsK+{;4FBP%3gM`gru}9bL*%&H1 zt@FXl1j+B*3=LMU+Z(G@Z_Izt*~wwVSN8aH{j%L^@^$Q+%@`QYOqym5mT{5H4C&es ze$4F1iWeK>e_uA^VmP34wEX?k+wqaX6^#!%J6DR_)NWgsZad9BSv8EUuAh-%#@5y4 z`##zV$0<}KC26^SJ9VciWB<1<edfTu><kQ>(=J%p_<uNZMCS7T@Mi_;S{oNBb*Hx! zGB8ZpbhYUFQRdg&%1!Ehu0LNG8#)+NOQdW3`TXY}6GOnI<=f?FY~=z=9z9`f+G6$X z^-K<~_qJQ)7#OB3eag3ubDo8msLA;&0k*=Tk$q)%m)+uIXsGO%D6U<;Z`-vJ{|`qt z9+|yDAvs!Can6C%*`*8&32{l+CReWh;`iam#+{ApUzeDKX5C)H%)szj=sQ=`yBBjG zbS4YiTop~?h-q89+_&%)2Lr<y?=#|8i`>t%_{+>zSiH`(W4Z3$_SUvF%nTbiE*7PS zaPAedDa;A9yi~lusQ0Sz-uBkL?D-4~I&IUetJErzv~0sCT#<P6Mb75yzh8_D5pygu zZ+#D}Qh~_a+40}tV5rZs=UQ8@9+$q#)5OlOz&G&glBa$5IOfgpTHje)6S1at_rwQY zYS&-S19!wOK5~5V>FxSA%&~$thEppZ_jpaZcSYvjm57z5+@K<S!ojQ7_bW8K!dd)% z)*d%MJF9v5iYu=+?lG@=#mJz*tXuxY<-?IHRclmM+UC4od-t5MDg%R2=grko?_RV% z=v?_V%u{Hg?_1|p)!g&u$T2ZEn1KddZwUQTu1Hd=TBVWsaO<})MsRMHbUiJ*#qYxr zpD!iJ_np4_gyeh;Vh#Ds*}}lEMR(tFz50&_^e&qm6gIx--66Pi!^Qr)O>wK{f!c-& zoBi|cFBG|7X7NAcqOZPmtH_40LSnPtvoJW6dC$MU-QC3d!;v#z0wOO3bqG$4wzY{2 z1g+C5`z}{!zUI!)TXq*!4hkDz{>A(6*Rk(s&vIq$mD2dOX6iK$28LT=o98|Ht-ZzV z!;v#v<dU{nINY7b$l$<N$S@)JUB<)*oijU6O`0bBOU!xJiU*t085kxwZ@Kf6&pVsN z|4hhfQMK*YZvA1%s5}pH<^r?8uU8Hq^!@T!{M)R^F6qDC`|U5^nYHcD6~(yd?rO2- z%Zp<q8}^*r`)~C_`~PJZ`E)PH|Et@5`#N9b+bwtQ{`p?N+1<qP!x5jYQWv(!*qBD2 zTzZRBVee%9{U&?LpP8F+Jz)JPYx~a8&Zhj<r2yNtWoJT`eAbn_c)x0XY4>{nd>aM^ z)97D+_D!>%rBIRdENZpM%Y~_{k4{iyGiG9NSo`S1vZuG>Z`!X?2T4kY)jmF5uePIW zQxx~S5Nk$;3r(xb_Zd1hz2clVW9PLrwyia<Zueb_QmM6IX1LHLVZvuBY!m3BZT|1I z^nL#9``gSnXW8q9mVFfvV`8|_zINJfeYqRCt5ho%b%f>psm{8Ym3lsjAz43Bk%6K1 z$Cq!-YW#(Z_?y(bGE_IbTv?=cXx54ktH4#j7LU`iTZL=_U0Niz%Kw$Te*e~<Rg1R> z+?oN3-|k0p-lt_vg=_+S-ZDD$Pc_yJsQ`^<Wn4Wqcm3=aK@0So)Vo&NGQ2Q6(80BS zmiE<0&K7O@Obia)k4&<3%dfb7a53B(&hynud&RTsxu8bW+TGtj>|T3HeMReo&ZI@3 zmRvKsu%fJMIj8NaotIWGPyE2Z@I^b#_Nra*^g51t8mq1;EO>ru=B)VLpxWw-!)aMl zL7TvkwT=$w4+htCT-}jx8@`f};llhG=d-S`m<reghV<sQtnN48B_9y`cpoSWE-(k@ z&^IjpBBG)if={lv><(LcaB?j(!vf>N{ae5MtiA1*)ui6FT3294;s3;@)e&oMu^m1h zJkR1TGXq1_=bO{{ZCg}L4+<Z>dWA`p@z|~HrJ)&9gMzP4=8s}vs9N^4?+lB-h`96n ztVE0Z#-^clpzwJSS)!wJ|KF+i44RztW^_k2F&?~Z9QZTzbg*#qQAUOrcduIC4>6CQ zZpQ(Uooal3wPHtiWXLULMuvplPx&?p+8BzPvPcy67HYAaU+-DAYAypqLbT0<*nf{2 zuPTTh6fO>QzCF!s_ui}(48cp;7#PmiRQ+z8W*sDGV`zSpnThdOTp;5#+X=!FObiD; zPqW*f@9N?F;mDm7zOqN14s%!z#?5S=d&B(gL<WWrd#d^OONYIy$qn^z1S>HJIV+=6 zoUp=+mx1A%_~v<!6y!jSgkoV)*U0-nSp3<$Yfs-&&f8$Y!0<-jig`|vJ6n_bGp*!+ z2NwSpr}93_Tg%DA$nZenV$pL&>u$A*q;*Ey+*Z!Aj5@}z4H~>H*s`MPRNWzqTwxo- za7kt!e}+CW(Zrj#R9M>>7z#{u%XMZw=-k<5zSrk-lhV)Y>rCh6IQAb9U|={V{M}}s z{br3O^=Ep)-BZfeeoXzfpzFYe7(E6CiTjtHf8Tm%qX1a4LGi16yp8I~PmZbvEDQ}N zk7Qmwy?y_ttTS^Tbnfh)EV@a+##hbzLGpbDhJ>Q<_cf0d^@_`rcGSGxn<(+`@+D(` z=v?&D*7cRk1lF+SkasC{*{H9qqUXLn^{z@hB@HJKLQZwIfueyv*k@u^Rh-f{Q* zZv-BGKL35g=83uMdoO0phb~0fa*Gjk5E)VHQk0sOt4{-;FcWFLapz9UIXjAKFP{P} z|1t260w15C@o*)?T%oY?rsd10iGvQsiM+)*FKjLNl%~x~u7VFN5_<QJ#s4)_S_rZ< z=Fl&{4_}Ua0vqT2x~&^@woX7(7kK$hTK?x-Pv1Vs{Cq1F)bV9tICE-o;`M@$iO=Oh zTlx=7xfaheZ%z*A5TgS+cVy!@=Dmvw1IeU092Ex9u5UQ!@koH=6S8Js*R#K$^*qHI zB$1Yo`@H{$zW$%c?`!lx0>M9a?s$3hf#+4QzRMN{6-Aq5K@yjLSjGoIPBmI%bJy1W z&rXngo?5(mHxZP`86HT?d%diB<D<+Ac_4cZ7=m&e1B1q+6AxZ4D+LK9IGsORF7=Ij zp4s}?IwPLLv!;R%YGSJHn)z*Yh13@CxpM{!Z7VN19=yz}4LT9%0L!t}6;fXx+iu+i zazjsIuF1Zamh7Q6ylp+805VusyZbq429bfmC;Pt4%OlU8J!`Cg*QtGdvCAt{Z+_5m zOpJ@pzPnTR|8k<JfAcDEaP}2EP%L$4_16vq$ELH4eudQByzeK#ak-&C`uE(l?T=S< zEq}P-3Ku8_*U4Hf^8~MFI#6hHcTd^R3wK_e0Q>%{#_UahKrVYzl(O-X1>A<Xf+q`0 zl3D#{U)Q^Dk^(xutYKpGV!fTtz0n^{dL}1<65WN?jYW$*o0ey6uYOey3ZDhp{buW9 z_gSA~>XEg{joba_$QqDy8{X;m?`D`BV#9k2oXB>GUG+;ASMwFI>1}hXz4n_66c8m@ zixw^M)SlPod1@udJsHO?dmcCNm9be_1xisbE}hC?`*EW$_@l`Rh`_1EkI($G3;VF- z6=+4)0`1e2!>oCOLu_~#uUfvm?l{PdS!NeK%XZ6JS4f50`o<)IBFiCkT3_|X%OGXY zB;qjpU2fd&n$J7d=d5~=dF6Kbn=7D*Yq+xPRPD006;f`tb>IK=fs8YGy7_GO^zh}C zj=}8y%B#T3#BO~o5vx4!)3iMBSLxQdTR~R5xOFMx$fc09(2pY19#`zWR}K<fQ1snx z@ziBzn;)E906N>R;gxIm-KCLxJ0inuco#qZns@fodC=Kb33*{=>to~QU*(^j`N5<p zb4nj5qyzdhHb&2Tdi9)5h1AwNXV<8JUHB;NR?_yxE=Mo>ot_)~4&=gD8*EOC?Ur?I zT0T+ZY?Sto=b(@^*c(0jy59OJ5yz7bUS2oN`ZL(!MO7h1DM2T@mUGMxYfp^O`e_U@ zz+xM}`%!0C5#D)iZ$G>|X23K5W<4kYh5WP9lG0jM@xf$M$~{oVa*&!ob=K=Gw_}=? z2cEpv{>&ESOr1No_P8mZUHvFiLiXhMhkNSeKxS6k=-OV-+`g05|8Uull5&Z*{bxR> zgW~kV=I?Gt9g7XGKgcx50VT17z<)tiGk$bGle6Ky{qVBtLy%1e?))hY;V}v4=A7p? zrv#MD0`A55ZMc2@P}A~@0;|8b_=7AtT4VOgCc48l%!YG5h<_?F<AsP{PF|i~O}&52 z&blIrHj9t%-W<{gt$JKgmp%Ks-u&n<t0OXwUhWHgdb$0ufz8<hTaZrCXz9`!x-nf3 zGH>wBzqa)*$YVaK+9gt=KXqbefknew!FC=m(hh!Mk}~t-7bRn`rD~f#I6nIA8TxU{ z;mmT7S;^j#R=knT%OC#y@NzTDbWm(exVSvhdAVZt>bu>3F+1y$CEDzt?*f&r4XwXU zKihiR+$i7iR@d^oZYS@?e~u9Z<^MxAvTYCfZ#pkuI1OTbTjGz$8HFH+Rc$e|+W9Vz zcl}<7%kreGK;?7Et7{r1QEQ(*4L5xJ(p7z3jJ+j~^OVW86JDPJIjgHf@zLq6y17#i zUhWePU#HJ|_=9~>3@^y8Ge=J><j>ArTOD}y;N=anc5|iM6r(=e^C$qHRk*n*U&P9G zWvT0G{&{R`L0)vIUX`=9I#3v5QoK}~qTl{%-Q%Jl$N4<DxZ>2xS&Wa`SH23_y*MXs zw@j%2bGM}#AUBxql-lX0z3}pdsH2zj79V_kdKD-L8=fpW^)hHJ^AY~bIUh`B`~_u+ zC)bbYEN?cEwRvk%`1DuxVun*E!8WsfcMJ7<t^3|h9Ade9(~c#7LB5Zekg+=Y)}FGT zY5dX`LO+<~wEHi2-#q`{oCigqv}|Y7JNZX+-s;=_<p(eK1u}@fEr0)PhZ5*$!ob3) zyPuwIJ?&q0`$1mwa@oZ{{ZBJ#fDYq3W+%<+=X<`;%IEvR%Y2L?prb35@3Q+@v-s;v zum%`5{Amsm2c@l)ZzU#|(p?{X>3Q&yVRzev`WGTLpc6M43cc=nwfNbdE3oQ%@R9=( zCx@2?ie8aksShf}XH2hN{A$_kus;b+%Q<^)eNbxoRctB_ij*^6A?A0l_?g=sylmLB zYJr_hYe3sXW(Ed_-ShemEt|b+wwijytV<bUSNu3ix4jYvr81ty8(zL!ci`lkWywM| zXD_VtTJht2b=xG6CR4e$ExuZ_)7(FVT*y$n;>WR63RFiF?KI_@{Vp$V_nxf$H2)7K z>Wi~h6fYKdWxIJH$p7mWEy@hNU=rW7oYkuE>93uE5~WolZb$3)UwqpHN;f%ccn@#b zr=1a1&UH75W1f)I))#Z{h<eSHi(RUv$-uyHL+;<+;Eg5a*X!*APfqE6u+nMwt#4)3 w>%W&WGB7OA^e=1mjt12o+1=VkJcs|Y|87axaQ;^}DCihGUHx3vIVCg!0079YUjP6A literal 0 HcmV?d00001 diff --git a/lib/python/docs/templates/sidebar/feedback.html b/lib/python/docs/templates/sidebar/feedback.html new file mode 100644 index 0000000000..d53ef29b9c --- /dev/null +++ b/lib/python/docs/templates/sidebar/feedback.html @@ -0,0 +1,7 @@ +<div class="sidebar-div"> + <p class="sidebar-heading">Feedback</p> + <p class="sidebar-text"> + Do you have a suggestion to improve this library or DBRepo? + <a href="https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/contact/#team">Give us feedback</a>. + </p> +</div> \ No newline at end of file diff --git a/lib/python/pyproject.toml b/lib/python/pyproject.toml new file mode 100644 index 0000000000..34fa50c5f3 --- /dev/null +++ b/lib/python/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "dbrepo" +version = "__APPVERSION__" +description = "DBRepo Python Library" +keywords = [ + "DBRepo", + "Database Repository" +] +authors = [ + { name = "Martin Weise, TU Wien", email = "martin.weise@tuwien.ac.at" } +] +readme = "README.md" +license = { file = "LICENSE" } +classifiers = [ + "Development Status :: 3 - Alpha", + "Topic :: Software Development :: Libraries", + "Programming Language :: Python :: 3.11", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", +] +requires-python = ">=3.11" +dependencies = [ + "requests >= 2.31", + "dataclasses", + "dataclasses-json", + "tuspy" +] + +[build-system] +requires = [ + "setuptools >= 61.0" +] +build-backend = "setuptools.build_meta" + +[project.urls] +Homepage = "https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/1.4.2/" +Documentation = "https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/1.4.2/sphinx/" +Issues = "https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/issues" +Source = "https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/" \ No newline at end of file diff --git a/lib/python/sensor.csv b/lib/python/sensor.csv new file mode 100644 index 0000000000..9a3b18d2d9 --- /dev/null +++ b/lib/python/sensor.csv @@ -0,0 +1,5 @@ +date,precipitation,lat,lng +2024-03-19,1.3,48.19482170115862,16.370144073925285 +2024-03-20,3.4,48.19482170115862,16.370144073925285 +2024-03-21,0,48.19482170115862,16.370144073925285 +2024-03-22,0,48.19482170115862,16.370144073925285 \ No newline at end of file diff --git a/lib/python/setup.py b/lib/python/setup.py new file mode 100644 index 0000000000..5f1c4834b4 --- /dev/null +++ b/lib/python/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +from distutils.core import setup + +setup(name="dbrepo", + version="__APPVERSION__", + description="A library for communicating with DBRepo", + url="https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//", + author="Martin Weise", + license="Apache-2.0", + author_email="martin.weise@tuwien.ac.at", + packages=[ + "dbrepo", + "dbrepo.api" + ]) diff --git a/lib/python/test.sh b/lib/python/test.sh new file mode 100644 index 0000000000..532d9a58d1 --- /dev/null +++ b/lib/python/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source ./lib/python/venv/bin/activate +cd ./lib/python/ && coverage run -m pytest tests/*.py --junitxml=report.xml && coverage html --omit="test/*" && coverage report --omit="test/*" > ./coverage.txt \ No newline at end of file diff --git a/lib/python/tests/test_analyse.py b/lib/python/tests/test_analyse.py new file mode 100644 index 0000000000..a6e98e491c --- /dev/null +++ b/lib/python/tests/test_analyse.py @@ -0,0 +1,24 @@ +import unittest +import requests_mock +import dataclasses + +from dbrepo.RestClient import RestClient + +from dbrepo.api.dto import KeyAnalysis + + +class AnalyseTest(unittest.TestCase): + + def test_analyse_keys_succeeds(self): + with requests_mock.Mocker() as mock: + exp = KeyAnalysis(keys={'id': 0, 'firstname': 1, 'lastname': 2}) + # mock + mock.get('/api/analyse/keys', json=dataclasses.asdict(exp), status_code=202) + # test + response = RestClient().analyse_keys(file_path='f705a7bd0cb2d5e37ab2b425036810a2', separator=',', + upload=False) + self.assertEqual(exp, response) + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/python/tests/test_database.py b/lib/python/tests/test_database.py new file mode 100644 index 0000000000..06efc661e8 --- /dev/null +++ b/lib/python/tests/test_database.py @@ -0,0 +1,572 @@ +import unittest +import requests_mock +import dataclasses + +from dbrepo.RestClient import RestClient + +from dbrepo.api.dto import Database, User, Container, Image, UserAttributes, DatabaseAccess, AccessType +from dbrepo.api.exceptions import ResponseCodeError, NotExistsError, ForbiddenError, MalformedError, AuthenticationError + + +class DatabaseTest(unittest.TestCase): + + def test_get_databases_empty_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database', json=[]) + # test + response = RestClient().get_databases() + self.assertEqual([], response) + + def test_get_databases_succeeds(self): + exp = [ + Database( + id=1, + name='test', + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + owner=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + contact=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + created='2024-01-01 00:00:00', + exchange_name='dbrepo', + internal_name='test_abcd', + is_public=True, + container=Container( + id=1, + name='MariaDB Galera 11.1.3', + internal_name='mariadb', + host='data-db', + port=3306, + sidecar_host='data-db-sidecar', + sidecar_port=3305, + created='2024-01-01 00:00:00', + image=Image( + id=1, + registry='docker.io', + name='mariadb', + version='11.2.2', + dialect='org.hibernate.dialect.MariaDBDialect', + driver_class='org.mariadb.jdbc.Driver', + jdbc_method='mariadb', + default_port=3306 + ) + ) + ) + ] + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database', json=[dataclasses.asdict(exp[0])]) + # test + response = RestClient().get_databases() + self.assertEqual(exp, response) + + def test_get_database_succeeds(self): + exp = Database( + id=1, + name='test', + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + owner=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + contact=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + created='2024-01-01 00:00:00', + exchange_name='dbrepo', + internal_name='test_abcd', + is_public=True, + container=Container( + id=1, + name='MariaDB Galera 11.1.3', + internal_name='mariadb', + host='data-db', + port=3306, + sidecar_host='data-db-sidecar', + sidecar_port=3305, + created='2024-01-01 00:00:00', + image=Image( + id=1, + registry='docker.io', + name='mariadb', + version='11.2.2', + dialect='org.hibernate.dialect.MariaDBDialect', + driver_class='org.mariadb.jdbc.Driver', + jdbc_method='mariadb', + default_port=3306 + ) + ) + ) + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1', json=dataclasses.asdict(exp)) + # test + response = RestClient().get_database(1) + self.assertEqual(exp, response) + + def test_get_database_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1', status_code=404) + # test + try: + response = RestClient().get_database(1) + except NotExistsError as e: + pass + + def test_get_database_invalid_dto_fails(self): + try: + exp = Database() + except TypeError as e: + pass + + def test_get_database_unauthorized_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1', status_code=401) + # test + try: + response = RestClient().get_database(1) + except ResponseCodeError as e: + pass + + def test_create_database_succeeds(self): + exp = Database( + id=1, + name='test', + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + owner=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + contact=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + created='2024-01-01 00:00:00', + exchange_name='dbrepo', + internal_name='test_abcd', + is_public=True, + container=Container( + id=1, + name='MariaDB Galera 11.1.3', + internal_name='mariadb', + host='data-db', + port=3306, + sidecar_host='data-db-sidecar', + sidecar_port=3305, + created='2024-01-01 00:00:00', + image=Image( + id=1, + registry='docker.io', + name='mariadb', + version='11.2.2', + dialect='org.hibernate.dialect.MariaDBDialect', + driver_class='org.mariadb.jdbc.Driver', + jdbc_method='mariadb', + default_port=3306 + ) + ) + ) + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database', json=dataclasses.asdict(exp), status_code=201) + # test + client = RestClient(username="a", password="b") + response = client.create_database(name='test', container_id=1, is_public=True) + self.assertEqual(response.name, 'test') + + def test_create_database_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_database(name='test', container_id=1, is_public=True) + except ForbiddenError as e: + pass + + def test_create_database_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_database(name='test', container_id=1, is_public=True) + except NotExistsError as e: + pass + + def test_create_database_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database', status_code=404) + # test + try: + response = RestClient().create_database(name='test', container_id=1, is_public=True) + except AuthenticationError as e: + pass + + def test_update_database_visibility_succeeds(self): + exp = Database( + id=1, + name='test', + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + owner=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + contact=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + created='2024-01-01 00:00:00', + exchange_name='dbrepo', + internal_name='test_abcd', + is_public=True, + container=Container( + id=1, + name='MariaDB Galera 11.1.3', + internal_name='mariadb', + host='data-db', + port=3306, + sidecar_host='data-db-sidecar', + sidecar_port=3305, + created='2024-01-01 00:00:00', + image=Image( + id=1, + registry='docker.io', + name='mariadb', + version='11.2.2', + dialect='org.hibernate.dialect.MariaDBDialect', + driver_class='org.mariadb.jdbc.Driver', + jdbc_method='mariadb', + default_port=3306 + ) + ) + ) + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1', json=dataclasses.asdict(exp), status_code=202) + # test + client = RestClient(username="a", password="b") + response = client.update_database_visibility(database_id=1, is_public=True) + self.assertEqual(response.is_public, True) + + def test_update_database_visibility_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_database_visibility(database_id=1, is_public=True) + except ForbiddenError: + pass + + def test_update_database_visibility_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_database_visibility(database_id=1, is_public=True) + except NotExistsError: + pass + + def test_update_database_visibility_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1', status_code=404) + # test + try: + response = RestClient().update_database_visibility(database_id=1, is_public=True) + except AuthenticationError: + pass + + def test_update_database_owner_succeeds(self): + exp = Database( + id=1, + name='test', + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + owner=User(id='abdbf897-e599-4e5a-a3f0-7529884ea011', username='other', + attributes=UserAttributes(theme='light')), + contact=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + created='2024-01-01 00:00:00', + exchange_name='dbrepo', + internal_name='test_abcd', + is_public=True, + container=Container( + id=1, + name='MariaDB Galera 11.1.3', + internal_name='mariadb', + host='data-db', + port=3306, + sidecar_host='data-db-sidecar', + sidecar_port=3305, + created='2024-01-01 00:00:00', + image=Image( + id=1, + registry='docker.io', + name='mariadb', + version='11.2.2', + dialect='org.hibernate.dialect.MariaDBDialect', + driver_class='org.mariadb.jdbc.Driver', + jdbc_method='mariadb', + default_port=3306 + ) + ) + ) + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/owner', json=dataclasses.asdict(exp), 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') + self.assertEqual(response.owner.id, 'abdbf897-e599-4e5a-a3f0-7529884ea011') + + def test_update_database_owner_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/owner', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_database_owner(database_id=1, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except ForbiddenError: + pass + + def test_update_database_owner_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/owner', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_database_owner(database_id=1, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except NotExistsError: + pass + + def test_update_database_owner_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/owner', status_code=404) + # test + try: + response = RestClient().update_database_owner(database_id=1, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except AuthenticationError: + pass + + def test_get_database_access_succeeds(self): + exp = DatabaseAccess(type=AccessType.READ, created='2024-01-01 00:00:00', + user=User(id='abdbf897-e599-4e5a-a3f0-7529884ea011', username='other', + attributes=UserAttributes(theme='light'))) + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/access', json=dataclasses.asdict(exp)) + # test + response = RestClient().get_database_access(database_id=1) + self.assertEqual(response, AccessType.READ) + + def test_get_database_access_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/access', status_code=403) + # test + try: + response = RestClient().get_database_access(database_id=1) + except ForbiddenError: + pass + + def test_get_database_access_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/access', status_code=404) + # test + try: + response = RestClient().get_database_access(database_id=1) + except NotExistsError: + pass + + def test_create_database_access_succeeds(self): + exp = DatabaseAccess(type=AccessType.READ, created='2024-01-01 00:00:00', + user=User(id='abdbf897-e599-4e5a-a3f0-7529884ea011', username='other', + attributes=UserAttributes(theme='light'))) + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', json=dataclasses.asdict(exp), + status_code=202) + # test + client = RestClient(username="a", password="b") + response = client.create_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + self.assertEqual(response, exp.type) + + def test_create_database_access_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except MalformedError: + pass + + def test_create_database_access_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=400) + # test + try: + response = RestClient().create_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except AuthenticationError: + pass + + def test_create_database_access_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except ForbiddenError: + pass + + def test_create_database_access_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except NotExistsError: + pass + + def test_update_database_access_succeeds(self): + exp = DatabaseAccess(type=AccessType.READ, created='2024-01-01 00:00:00', + user=User(id='abdbf897-e599-4e5a-a3f0-7529884ea011', username='other', + attributes=UserAttributes(theme='light'))) + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', json=dataclasses.asdict(exp), + status_code=202) + # test + client = RestClient(username="a", password="b") + response = client.update_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + self.assertEqual(response, exp.type) + + def test_update_database_access_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except MalformedError: + pass + + def test_update_database_access_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except ForbiddenError: + pass + + def test_update_database_access_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except NotExistsError: + pass + + def test_update_database_access_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=404) + # test + try: + response = RestClient().update_database_access(database_id=1, type=AccessType.READ, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except AuthenticationError: + pass + + def test_delete_database_access_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=202) + # test + client = RestClient(username="a", password="b") + response = client.delete_database_access(database_id=1, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + + def test_delete_database_access_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + response = client.delete_database_access(database_id=1, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except MalformedError: + pass + + def test_delete_database_access_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.delete_database_access(database_id=1, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except ForbiddenError: + pass + + def test_delete_database_access_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.delete_database_access(database_id=1, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except NotExistsError: + pass + + def test_delete_database_access_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/access/abdbf897-e599-4e5a-a3f0-7529884ea011', status_code=404) + # test + try: + response = RestClient().delete_database_access(database_id=1, + user_id='abdbf897-e599-4e5a-a3f0-7529884ea011') + except AuthenticationError: + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/python/tests/test_identifier.py b/lib/python/tests/test_identifier.py new file mode 100644 index 0000000000..72a2e0380e --- /dev/null +++ b/lib/python/tests/test_identifier.py @@ -0,0 +1,195 @@ +import unittest +import requests_mock +import dataclasses + +from dbrepo.RestClient import RestClient + +from dbrepo.api.dto import Identifier, IdentifierType, CreateIdentifierTitle, CreateIdentifierCreator, \ + IdentifierCreator, IdentifierTitle, IdentifierDescription, CreateIdentifierDescription, Language, \ + CreateIdentifierFunder, CreateRelatedIdentifier, RelatedIdentifierRelation, RelatedIdentifierType, IdentifierFunder, \ + RelatedIdentifier +from dbrepo.api.exceptions import MalformedError, ForbiddenError, NotExistsError, ExternalSystemError, \ + AuthenticationError + + +class IdentifierTest(unittest.TestCase): + + def test_create_identifier_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Identifier(id=10, + database_id=1, + view_id=32, + publication_year=2024, + publisher='TU Wien', + created='2022-01-01 00:00:00', + last_modified='2022-01-01 00:00:00', + type=IdentifierType.VIEW, + language=Language.EN, + descriptions=[IdentifierDescription(id=2, description='Test Description')], + titles=[IdentifierTitle(id=3, title='Test Title')], + funders=[IdentifierFunder(id=4, funder_name='FWF')], + related_identifiers=[ + RelatedIdentifier(id=7, value='10.12345/abc', relation=RelatedIdentifierRelation.CITES, + type=RelatedIdentifierType.DOI)], + creators=[IdentifierCreator(id=5, creator_name='Carberry, Josiah')]) + # mock + mock.post('/api/identifier', json=dataclasses.asdict(exp), status_code=201) + # test + client = RestClient(username="a", password="b") + response = client.create_identifier(database_id=1, type=IdentifierType.VIEW, + titles=[CreateIdentifierTitle(title='Test Title')], + publisher='TU Wien', publication_year=2024, + language=Language.EN, + funders=[CreateIdentifierFunder(funder_name='FWF')], + related_identifiers=[CreateRelatedIdentifier(value='10.12345/abc', + relation=RelatedIdentifierRelation.CITES, + type=RelatedIdentifierType.DOI)], + descriptions=[ + CreateIdentifierDescription(description='Test Description')], + creators=[ + CreateIdentifierCreator(creator_name='Carberry, Josiah')]) + self.assertEqual(exp, response) + + def test_create_identifier_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/identifier', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_identifier(database_id=1, type=IdentifierType.VIEW, + titles=[CreateIdentifierTitle(title='Test Title')], + publisher='TU Wien', publication_year=2024, + creators=[ + CreateIdentifierCreator(creator_name='Carberry, Josiah')]) + except MalformedError: + pass + + def test_create_identifier_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/identifier', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_identifier(database_id=1, type=IdentifierType.VIEW, + titles=[CreateIdentifierTitle(title='Test Title')], + publisher='TU Wien', publication_year=2024, + creators=[ + CreateIdentifierCreator(creator_name='Carberry, Josiah')]) + except ForbiddenError: + pass + + def test_create_identifier_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/identifier', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_identifier(database_id=1, type=IdentifierType.VIEW, + titles=[CreateIdentifierTitle(title='Test Title')], + publisher='TU Wien', publication_year=2024, + creators=[ + CreateIdentifierCreator(creator_name='Carberry, Josiah')]) + except NotExistsError: + pass + + def test_create_identifier_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/identifier', status_code=503) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_identifier(database_id=1, type=IdentifierType.VIEW, + titles=[CreateIdentifierTitle(title='Test Title')], + publisher='TU Wien', publication_year=2024, + creators=[ + CreateIdentifierCreator(creator_name='Carberry, Josiah')]) + except ExternalSystemError: + pass + + def test_create_identifier_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/identifier', status_code=503) + # test + try: + response = RestClient().create_identifier(database_id=1, type=IdentifierType.VIEW, + titles=[CreateIdentifierTitle(title='Test Title')], + publisher='TU Wien', publication_year=2024, + creators=[ + CreateIdentifierCreator(creator_name='Carberry, Josiah')]) + except AuthenticationError: + pass + + def test_suggest_identifier_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Identifier(id=10, + database_id=1, + publication_year=2024, + publisher='TU Wien', + created='2022-01-01 00:00:00', + last_modified='2022-01-01 00:00:00', + type=IdentifierType.VIEW, + 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=dataclasses.asdict(exp)) + # test + response = RestClient().suggest_identifier("https://orcid.org/0000-0002-1825-0097") + self.assertEqual(exp, response) + + def test_suggest_identifier_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/identifier?url=https://orcid.org/0000-0002-1825-0097', status_code=404) + # test + try: + response = RestClient().suggest_identifier("https://orcid.org/0000-0002-1825-0097") + except NotExistsError: + pass + + def test_get_identifiers_succeeds(self): + with requests_mock.Mocker() as mock: + exp = [Identifier(id=10, + database_id=1, + view_id=32, + publication_year=2024, + publisher='TU Wien', + created='2022-01-01 00:00:00', + last_modified='2022-01-01 00:00:00', + type=IdentifierType.VIEW, + language=Language.EN, + descriptions=[IdentifierDescription(id=2, description='Test Description')], + titles=[IdentifierTitle(id=3, title='Test Title')], + funders=[IdentifierFunder(id=4, funder_name='FWF')], + related_identifiers=[ + RelatedIdentifier(id=7, value='10.12345/abc', + relation=RelatedIdentifierRelation.CITES, + type=RelatedIdentifierType.DOI)], + creators=[IdentifierCreator(id=5, creator_name='Carberry, Josiah')])] + # mock + mock.get('/api/pid', json=[dataclasses.asdict(exp[0])], headers={"Accept": "application/json"}) + # test + response = RestClient().get_identifiers() + self.assertEqual(exp, response) + + def test_get_identifiers_ld_json_succeeds(self): + with requests_mock.Mocker() as mock: + exp = [{"@context": "https://schema.org/", "@type": "Dataset", "url": "http://localhost/database/2/info", + "citation": "http://localhost/pid/2", "hasPart": [], "version": "2024-03-21T12:05:46.000Z", + "name": "sdfsdf", "description": "sfsdf", "identifier": ["http://localhost/pid/2"], + "license": "https://creativecommons.org/licenses/by/4.0/legalcode", "creator": [ + {"name": "Weise, Martin", "@type": "Person", "sameAs": "https://orcid.org/0000-0003-4216-302X", + "givenName": "Martin", "familyName": "Weise"}], "temporalCoverage": 2024}] + # mock + mock.get('/api/pid', json=exp, headers={"Accept": "application/ld+json"}) + # test + response = RestClient().get_identifiers(ld=True) + self.assertEqual(exp, response) + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/python/tests/test_license.py b/lib/python/tests/test_license.py new file mode 100644 index 0000000000..3a62cba74b --- /dev/null +++ b/lib/python/tests/test_license.py @@ -0,0 +1,33 @@ +import unittest +import requests_mock +import dataclasses + +from dbrepo.RestClient import RestClient + +from dbrepo.api.dto import Database, User, Container, Image, UserAttributes, DatabaseAccess, AccessType, License +from dbrepo.api.exceptions import ResponseCodeError, NotExistsError, ForbiddenError, MalformedError + + +class DatabaseTest(unittest.TestCase): + + def test_get_licenses_empty_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/license', json=[]) + # test + response = RestClient().get_licenses() + self.assertEqual([], response) + + def test_get_licenses_succeeds(self): + with requests_mock.Mocker() as mock: + exp = [License(identifier='CC-BY-4.0', uri='https://creativecommons.org/licenses/by/4.0/', + description='The Creative Commons Attribution license allows re-distribution and re-use of a licensed work on the condition that the creator is appropriately credited.')] + # mock + mock.get('/api/database/license', json=[dataclasses.asdict(exp[0])]) + # test + response = RestClient().get_licenses() + self.assertEqual(exp, response) + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/python/tests/test_query.py b/lib/python/tests/test_query.py new file mode 100644 index 0000000000..bad193efdd --- /dev/null +++ b/lib/python/tests/test_query.py @@ -0,0 +1,335 @@ +import unittest +import requests_mock +import dataclasses + +from dbrepo.RestClient import RestClient + +from dbrepo.api.dto import Result, Query, User, UserAttributes, QueryType +from dbrepo.api.exceptions import MalformedError, NotExistsError, ForbiddenError, QueryStoreError, \ + MetadataConsistencyError, AuthenticationError + + +class QueryTest(unittest.TestCase): + + def test_execute_query_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], + headers=[{'id': 0, 'username': 1}], + id=None) + # mock + mock.post('/api/database/1/query', json=dataclasses.asdict(exp), status_code=202) + # test + client = RestClient(username="a", password="b") + response = client.execute_query(database_id=1, page=0, size=10, timestamp=None, + query="SELECT id, username FROM some_table WHERE id IN (1,2)") + self.assertEqual(exp, response) + + def test_execute_query_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/query', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + response = client.execute_query(database_id=1, + query="SELECT id, username FROM some_table WHERE id IN (1,2)") + except MalformedError: + pass + + def test_execute_query_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/query', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.execute_query(database_id=1, + query="SELECT id, username FROM some_table WHERE id IN (1,2)") + except ForbiddenError: + pass + + def test_execute_query_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/query', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.execute_query(database_id=1, + query="SELECT id, username FROM some_table WHERE id IN (1,2)") + except NotExistsError: + pass + + def test_execute_query_not_valid_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/query', status_code=409) + # test + try: + client = RestClient(username="a", password="b") + response = client.execute_query(database_id=1, + query="SELECT id, username FROM some_table WHERE id IN (1,2)") + except QueryStoreError: + pass + + def test_execute_query_not_expected_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/query', status_code=417) + # test + try: + client = RestClient(username="a", password="b") + response = client.execute_query(database_id=1, + query="SELECT id, username FROM some_table WHERE id IN (1,2)") + except MetadataConsistencyError: + pass + + def test_execute_query_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/query', status_code=417) + # test + try: + response = RestClient().execute_query(database_id=1, + query="SELECT id, username FROM some_table WHERE id IN (1,2)") + except AuthenticationError: + pass + + def test_find_query_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Query(id=6, + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + execution='2024-01-01 00:00:00', + created='2024-01-01 00:00:00', + last_modified='2024-01-01 00:00:00', + query='SELECT id, username FROM some_table WHERE id IN (1,2)', + query_normalized='SELECT id, username FROM some_table WHERE id IN (1,2)', + type=QueryType.QUERY, + database_id=1, + query_hash='da5ff66c4a57683171e2ffcec25298ee684680d1e03633cd286f9067d6924ad8', + result_hash='464740ba612225913bb15b26f13377707949b55e65288e89c3f8b4c6469aecb4', + is_persisted=False, + result_number=None, + identifiers=[]) + # mock + mock.get('/api/database/1/query/6', json=dataclasses.asdict(exp)) + # test + response = RestClient().get_query(database_id=1, query_id=6) + self.assertEqual(exp, response) + + def test_find_query_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query/6', status_code=403) + # test + try: + response = RestClient().get_query(database_id=1, query_id=6) + except ForbiddenError: + pass + + def test_find_query_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query/6', status_code=404) + # test + try: + response = RestClient().get_query(database_id=1, query_id=6) + except NotExistsError: + pass + + def test_find_query_not_valid_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query/6', status_code=501) + # test + try: + response = RestClient().get_query(database_id=1, query_id=6) + except QueryStoreError: + pass + + def test_find_query_not_expected_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query/6', status_code=417) + # test + try: + response = RestClient().get_query(database_id=1, query_id=6) + except MetadataConsistencyError: + pass + + def test_find_queries_empty_succeeds(self): + with requests_mock.Mocker() as mock: + exp = [] + # mock + mock.get('/api/database/1/query', json=[]) + # test + response = RestClient().get_queries(database_id=1) + self.assertEqual(exp, response) + + def test_find_queries_succeeds(self): + with requests_mock.Mocker() as mock: + exp = [Query(id=6, + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + execution='2024-01-01 00:00:00', + created='2024-01-01 00:00:00', + last_modified='2024-01-01 00:00:00', + query='SELECT id, username FROM some_table WHERE id IN (1,2)', + query_normalized='SELECT id, username FROM some_table WHERE id IN (1,2)', + type=QueryType.QUERY, + database_id=1, + query_hash='da5ff66c4a57683171e2ffcec25298ee684680d1e03633cd286f9067d6924ad8', + result_hash='464740ba612225913bb15b26f13377707949b55e65288e89c3f8b4c6469aecb4', + is_persisted=False, + result_number=None, + identifiers=[])] + # mock + mock.get('/api/database/1/query', json=[dataclasses.asdict(exp[0])]) + # test + response = RestClient().get_queries(database_id=1) + self.assertEqual(exp, response) + + def test_find_queries_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query', status_code=403) + # test + try: + response = RestClient().get_queries(database_id=1) + except ForbiddenError: + pass + + def test_find_queries_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query', status_code=404) + # test + try: + response = RestClient().get_queries(database_id=1) + except NotExistsError: + pass + + def test_find_queries_not_valid_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query', status_code=501) + # test + try: + response = RestClient().get_queries(database_id=1) + except QueryStoreError: + pass + + def test_find_queries_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query', status_code=423) + # test + try: + response = RestClient().get_queries(database_id=1) + except MalformedError: + pass + + def test_get_query_data_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], + headers=[{'id': 0, 'username': 1}], + id=6) + # mock + mock.get('/api/database/1/query/6/data', json=dataclasses.asdict(exp)) + # test + response = RestClient().get_query_data(database_id=1, query_id=6) + self.assertEqual(exp, response) + + def test_get_query_data_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query/6/data', status_code=403) + # test + try: + response = RestClient().get_query_data(database_id=1, query_id=6) + except ForbiddenError: + pass + + def test_get_query_data_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query/6/data', status_code=404) + # test + try: + response = RestClient().get_query_data(database_id=1, query_id=6) + except NotExistsError: + pass + + def test_get_query_data_not_valid_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query/6/data', status_code=409) + # test + try: + response = RestClient().get_query_data(database_id=1, query_id=6) + except QueryStoreError: + pass + + def test_get_query_data_not_consistent_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/query/6/data', status_code=417) + # test + try: + response = RestClient().get_query_data(database_id=1, query_id=6) + except MetadataConsistencyError: + pass + + def test_get_query_data_count_succeeds(self): + with requests_mock.Mocker() as mock: + exp = 2 + # mock + mock.head('/api/database/1/query/6/data', headers={'X-Count': str(exp)}) + # test + response = RestClient().get_query_data_count(database_id=1, query_id=6) + self.assertEqual(exp, response) + + def test_get_query_data_count_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/query/6/data', status_code=403) + # test + try: + response = RestClient().get_query_data_count(database_id=1, query_id=6) + except ForbiddenError: + pass + + def test_get_query_data_count_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/query/6/data', status_code=404) + # test + try: + response = RestClient().get_query_data_count(database_id=1, query_id=6) + except NotExistsError: + pass + + def test_get_query_data_count_not_valid_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/query/6/data', status_code=409) + # test + try: + response = RestClient().get_query_data_count(database_id=1, query_id=6) + except QueryStoreError: + pass + + def test_get_query_data_count_not_consistent_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/query/6/data', status_code=417) + # test + try: + response = RestClient().get_query_data_count(database_id=1, query_id=6) + except MetadataConsistencyError: + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/python/tests/test_rest_client.py b/lib/python/tests/test_rest_client.py new file mode 100644 index 0000000000..5a57cc7118 --- /dev/null +++ b/lib/python/tests/test_rest_client.py @@ -0,0 +1,41 @@ +import os +from unittest import TestCase, mock, main + +from dbrepo.RestClient import RestClient + + +class DatabaseTest(TestCase): + + def test_constructor_succeeds(self): + # test + client = RestClient() + self.assertEqual("http://gateway-service", client.endpoint) + self.assertIsNone(client.username) + self.assertIsNone(client.password) + self.assertTrue(client.secure) + + @mock.patch.dict(os.environ, { + "DBREPO_ENDPOINT": "https://test.dbrepo.tuwien.ac.at", + "DBREPO_USERNAME": "foo", + "DBREPO_PASSWORD": "bar", + "DBREPO_SECURE": "False", + }) + def test_constructor_environment_succeeds(self): + # test + client = RestClient() + self.assertEqual("https://test.dbrepo.tuwien.ac.at", client.endpoint) + self.assertEqual("foo", client.username) + self.assertEqual("bar", client.password) + self.assertFalse(client.secure) + + def test_constructor_credentials_succeeds(self): + # test + client = RestClient(username='admin', password='pass') + self.assertEqual("http://gateway-service", client.endpoint) + self.assertEqual('admin', client.username) + self.assertEqual('pass', client.password) + self.assertTrue(client.secure) + + +if __name__ == "__main__": + main() diff --git a/lib/python/tests/test_table.py b/lib/python/tests/test_table.py new file mode 100644 index 0000000000..1b53d9ec40 --- /dev/null +++ b/lib/python/tests/test_table.py @@ -0,0 +1,651 @@ +import unittest +import requests_mock +import dataclasses + +from dbrepo.RestClient import RestClient + +from dbrepo.api.dto import Table, CreateTableConstraints, UserAttributes, User, Column, Constraints, ColumnType, Result, \ + Concept, Unit, TableStatistics, ColumnStatistic +from dbrepo.api.exceptions import MalformedError, ForbiddenError, NotExistsError, NameExistsError, QueryStoreError, \ + AuthenticationError + + +class TableTest(unittest.TestCase): + + def test_create_table_succeeds(self): + exp = Table(id=2, + name="Test", + description="Test Table", + database_id=1, + internal_name="test", + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + owner=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + created='2024-01-01 00:00:00', + is_versioned=True, + created_by='8638c043-5145-4be8-a3e4-4b79991b0a16', + queue_name='test', + routing_key='dbrepo.test_database_1234.test', + is_public=True, + constraints=Constraints(), + columns=[Column(id=1, + name="ID", + database_id=1, + table_id=2, + internal_name="id", + auto_generated=True, + is_primary_key=True, + column_type=ColumnType.BIGINT, + is_public=True, + is_null_allowed=False)]) + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table', json=dataclasses.asdict(exp), status_code=201) + # test + client = RestClient(username="a", password="b") + response = client.create_table(database_id=1, name="Test", description="Test Table", columns=[], + constraints=CreateTableConstraints()) + self.assertEqual(exp, response) + + def test_create_table_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_table(database_id=1, name="Test", description="Test Table", columns=[], + constraints=CreateTableConstraints()) + except MalformedError: + pass + + def test_create_table_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_table(database_id=1, name="Test", description="Test Table", columns=[], + constraints=CreateTableConstraints()) + except ForbiddenError: + pass + + def test_create_table_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_table(database_id=1, name="Test", description="Test Table", columns=[], + constraints=CreateTableConstraints()) + except NotExistsError: + pass + + def test_create_table_name_exists_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table', status_code=409) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_table(database_id=1, name="Test", description="Test Table", columns=[], + constraints=CreateTableConstraints()) + except NameExistsError: + pass + + def test_create_table_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table', status_code=409) + # test + try: + response = RestClient().create_table(database_id=1, name="Test", description="Test Table", columns=[], + constraints=CreateTableConstraints()) + except AuthenticationError: + pass + + def test_get_tables_empty_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/table', json=[]) + # test + response = RestClient().get_tables(database_id=1) + self.assertEqual([], response) + + def test_get_tables_succeeds(self): + with requests_mock.Mocker() as mock: + exp = [Table(id=2, + name="Test", + description="Test Table", + database_id=1, + internal_name="test", + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + owner=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + created='2024-01-01 00:00:00', + is_versioned=True, + created_by='8638c043-5145-4be8-a3e4-4b79991b0a16', + queue_name='test', + routing_key='dbrepo.test_database_1234.test', + is_public=True, + constraints=Constraints(), + columns=[Column(id=1, + name="ID", + database_id=1, + table_id=2, + internal_name="id", + auto_generated=True, + is_primary_key=True, + column_type=ColumnType.BIGINT, + is_public=True, + is_null_allowed=False)])] + # mock + mock.get('/api/database/1/table', json=[dataclasses.asdict(exp[0])]) + # test + response = RestClient().get_tables(database_id=1) + self.assertEqual(exp, response) + + def test_get_table_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Table(id=2, + name="Test", + description="Test Table", + database_id=1, + internal_name="test", + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + owner=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + created='2024-01-01 00:00:00', + is_versioned=True, + created_by='8638c043-5145-4be8-a3e4-4b79991b0a16', + queue_name='test', + routing_key='dbrepo.test_database_1234.test', + is_public=True, + constraints=Constraints(), + columns=[Column(id=1, + name="ID", + database_id=1, + table_id=2, + internal_name="id", + auto_generated=True, + is_primary_key=True, + column_type=ColumnType.BIGINT, + is_public=True, + is_null_allowed=False)]) + # mock + mock.get('/api/database/1/table/2', json=dataclasses.asdict(exp)) + # test + response = RestClient().get_table(database_id=1, table_id=2) + self.assertEqual(exp, response) + + def test_get_table_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/table/2', status_code=403) + # test + try: + response = RestClient().get_table(database_id=1, table_id=2) + except ForbiddenError: + pass + + def test_get_table_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/table/2', status_code=404) + # test + try: + response = RestClient().get_table(database_id=1, table_id=2) + except NotExistsError: + pass + + def test_delete_table_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/2', status_code=202) + # test + client = RestClient(username="a", password="b") + client.delete_table(database_id=1, table_id=2) + + def test_delete_table_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/2', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + client.delete_table(database_id=1, table_id=2) + except ForbiddenError: + pass + + def test_delete_table_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/2', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + client.delete_table(database_id=1, table_id=2) + except NotExistsError: + pass + + def test_delete_table_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/2', status_code=404) + # test + try: + RestClient().delete_table(database_id=1, table_id=2) + except AuthenticationError: + pass + + def test_get_table_data_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], + headers=[{'id': 0, 'username': 1}], + id=None) + # mock + mock.get('/api/database/1/table/9/data', json=dataclasses.asdict(exp)) + # test + response = RestClient().get_table_data(database_id=1, table_id=9) + self.assertEqual(exp, response) + + def test_get_table_data_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/table/9/data', status_code=400) + # test + try: + response = RestClient().get_table_data(database_id=1, table_id=9) + except MalformedError: + pass + + def test_get_table_data_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/table/9/data', status_code=403) + # test + try: + response = RestClient().get_table_data(database_id=1, table_id=9) + except ForbiddenError: + pass + + def test_get_table_data_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/table/9/data', status_code=404) + # test + try: + response = RestClient().get_table_data(database_id=1, table_id=9) + except NotExistsError: + pass + + def test_get_table_data_not_countable_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/table/9/data', status_code=409) + # test + try: + response = RestClient().get_table_data(database_id=1, table_id=9) + except QueryStoreError: + pass + + def test_get_table_data_count_succeeds(self): + with requests_mock.Mocker() as mock: + exp = 2 + # mock + mock.head('/api/database/1/table/9/data', headers={'X-Count': str(exp)}) + # test + response = RestClient().get_table_data_count(database_id=1, table_id=9) + self.assertEqual(exp, response) + + def test_get_table_data_count_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/table/9/data', status_code=400) + # test + try: + response = RestClient().get_table_data_count(database_id=1, table_id=9) + except MalformedError: + pass + + def test_get_table_data_count_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/table/9/data', status_code=403) + # test + try: + response = RestClient().get_table_data_count(database_id=1, table_id=9) + except ForbiddenError: + pass + + def test_get_table_data_count_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/table/9/data', status_code=404) + # test + try: + response = RestClient().get_table_data_count(database_id=1, table_id=9) + except NotExistsError: + pass + + def test_get_table_data_count_not_countable_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/table/9/data', status_code=409) + # test + try: + response = RestClient().get_table_data_count(database_id=1, table_id=9) + except QueryStoreError: + pass + + def test_create_table_data_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table/9/data', status_code=202) + # test + client = RestClient(username="a", password="b") + client.create_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}) + + def test_create_table_data_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table/9/data', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + client.create_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}) + except MalformedError: + pass + + def test_create_table_data_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table/9/data', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + client.create_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}) + except ForbiddenError: + pass + + def test_create_table_data_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table/9/data', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + client.create_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}) + except NotExistsError: + pass + + def test_create_table_data_not_lob_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table/9/data', status_code=410) + # test + try: + client = RestClient(username="a", password="b") + client.create_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}) + except MalformedError: + pass + + def test_create_table_data_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/table/9/data', status_code=410) + # test + try: + RestClient().create_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}) + except AuthenticationError: + pass + + def test_update_table_data_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/9/data', status_code=202) + # test + client = RestClient(username="a", password="b") + client.update_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}, + keys={'id': 1}) + + def test_update_table_data_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/9/data', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + client.update_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}, + keys={'id': 1}) + except MalformedError: + pass + + def test_update_table_data_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/9/data', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + client.update_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}, + keys={'id': 1}) + except ForbiddenError: + pass + + def test_update_table_data_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/9/data', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + client.update_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}, + keys={'id': 1}) + except NotExistsError: + pass + + def test_update_table_data_not_lob_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/9/data', status_code=410) + # test + try: + client = RestClient(username="a", password="b") + client.update_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}, + keys={'id': 1}) + except MalformedError: + pass + + def test_update_table_data_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/9/data', status_code=410) + # test + try: + RestClient().update_table_data(database_id=1, table_id=9, + data={'name': 'Josiah', 'age': 45, 'gender': 'male'}, + keys={'id': 1}) + except AuthenticationError: + pass + + def test_delete_table_data_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/9/data', status_code=202) + # test + client = RestClient(username="a", password="b") + client.delete_table_data(database_id=1, table_id=9, keys={'id': 1}) + + def test_delete_table_data_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/9/data', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + client.delete_table_data(database_id=1, table_id=9, keys={'id': 1}) + except MalformedError: + pass + + def test_delete_table_data_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/9/data', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + client.delete_table_data(database_id=1, table_id=9, keys={'id': 1}) + except ForbiddenError: + pass + + def test_delete_table_data_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/9/data', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + client.delete_table_data(database_id=1, table_id=9, keys={'id': 1}) + except NotExistsError: + pass + + def test_delete_table_data_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/table/9/data', status_code=404) + # test + try: + RestClient().delete_table_data(database_id=1, table_id=9, keys={'id': 1}) + except AuthenticationError: + pass + + def test_update_table_column_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Column(id=1, + name="ID", + database_id=1, + table_id=2, + internal_name="id", + auto_generated=True, + is_primary_key=True, + column_type=ColumnType.BIGINT, + is_public=True, + concept=Concept(id=2, + uri="http://dbpedia.org/page/Category:Precipitation", + created='2023-01-12 00:00:00', + name="Precipitation"), + unit=Unit(id=2, + uri="http://www.wikidata.org/entity/Q119856947", + created='2023-01-12 00:00:00', + name="liters per square meter"), + is_null_allowed=False) + # mock + mock.put('/api/database/1/table/2/column/1', json=dataclasses.asdict(exp), status_code=202) + # test + client = RestClient(username="a", password="b") + response = client.update_table_column(database_id=1, table_id=2, column_id=1, + unit_uri="http://www.wikidata.org/entity/Q119856947", + concept_uri="http://dbpedia.org/page/Category:Precipitation") + self.assertEqual(exp, response) + + def test_update_table_column_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/2/column/1', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + client.update_table_column(database_id=1, table_id=2, column_id=1, + unit_uri="http://www.wikidata.org/entity/Q119856947", + concept_uri="http://dbpedia.org/page/Category:Precipitation") + except MalformedError: + pass + + def test_update_table_column_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/2/column/1', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + client.update_table_column(database_id=1, table_id=2, column_id=1, + unit_uri="http://www.wikidata.org/entity/Q119856947", + concept_uri="http://dbpedia.org/page/Category:Precipitation") + except ForbiddenError: + pass + + def test_update_table_column_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/2/column/1', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + client.update_table_column(database_id=1, table_id=2, column_id=1, + unit_uri="http://www.wikidata.org/entity/Q119856947", + concept_uri="http://dbpedia.org/page/Category:Precipitation") + except NotExistsError: + pass + + def test_update_table_column_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('/api/database/1/table/2/column/1', status_code=404) + # test + try: + RestClient().update_table_column(database_id=1, table_id=2, column_id=1, + unit_uri="http://www.wikidata.org/entity/Q119856947", + concept_uri="http://dbpedia.org/page/Category:Precipitation") + except AuthenticationError: + pass + + def test_analyse_table_statistics_succeeds(self): + with requests_mock.Mocker() as mock: + 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=dataclasses.asdict(exp), status_code=202) + # test + response = RestClient().analyse_table_statistics(database_id=1, table_id=2) + self.assertEqual(exp, response) + + def test_analyse_table_statistics_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/analyse/database/1/table/2/statistics', status_code=400) + # test + try: + RestClient().analyse_table_statistics(database_id=1, table_id=2) + except MalformedError: + pass + + def test_analyse_table_statistics_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/analyse/database/1/table/2/statistics', status_code=404) + # test + try: + RestClient().analyse_table_statistics(database_id=1, table_id=2) + except NotExistsError: + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/python/tests/test_user.py b/lib/python/tests/test_user.py new file mode 100644 index 0000000000..0191150b5e --- /dev/null +++ b/lib/python/tests/test_user.py @@ -0,0 +1,321 @@ +import dataclasses +import unittest +import requests_mock + +from dbrepo.RestClient import RestClient +from dbrepo.api.dto import User, UserAttributes, UserBrief +from dbrepo.api.exceptions import ResponseCodeError, UsernameExistsError, EmailExistsError, NotExistsError, \ + ForbiddenError, AuthenticationError + + +class UserTest(unittest.TestCase): + + def test_whoami_fails(self): + username = RestClient().whoami() + self.assertIsNone(username) + + def test_whoami_succeeds(self): + client = RestClient(username="a", password="b") + username = client.whoami() + self.assertEqual("a", username) + + def test_get_users_empty_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('http://gateway-service/api/user', json=[]) + # test + response = RestClient().get_users() + self.assertEqual([], response) + + def test_get_user_succeeds(self): + with requests_mock.Mocker() as mock: + exp = [ + User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='dark')) + ] + # mock + mock.get('http://gateway-service/api/user', json=[dataclasses.asdict(exp[0])]) + # test + response = RestClient().get_users() + self.assertEqual(exp, response) + + def test_get_user_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('http://gateway-service/api/user', status_code=404) + # test + try: + response = RestClient().get_users() + except ResponseCodeError as e: + pass + + def test_create_user_succeeds(self): + 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=dataclasses.asdict(exp), status_code=201) + # test + response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') + self.assertEqual(exp, response) + + def test_create_user_bad_request_fails(self): + 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=dataclasses.asdict(exp), status_code=400) + # test + try: + response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') + except ResponseCodeError as e: + pass + + def test_create_user_not_allowed_fails(self): + 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=dataclasses.asdict(exp), status_code=403) + # test + try: + response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') + except ForbiddenError as e: + pass + + def test_create_user_username_exists_fails(self): + 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=dataclasses.asdict(exp), status_code=409) + # test + try: + response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') + except UsernameExistsError as e: + pass + + def test_create_user_default_role_not_exists_fails(self): + 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=dataclasses.asdict(exp), status_code=404) + # test + try: + response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') + except NotExistsError as e: + pass + + def test_create_user_emails_exists_fails(self): + 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=dataclasses.asdict(exp), status_code=417) + # test + try: + response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') + except EmailExistsError as e: + pass + + def test_get_user_succeeds(self): + with requests_mock.Mocker() as mock: + exp = User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='dark')) + # mock + mock.get('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', + json=dataclasses.asdict(exp)) + # test + response = RestClient().get_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16') + self.assertEqual(exp, response) + + def test_get_user_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', status_code=404) + # test + try: + response = RestClient().get_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16') + except NotExistsError as e: + pass + + def test_update_user_succeeds(self): + with requests_mock.Mocker() as mock: + exp = User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', given_name='Martin', + attributes=UserAttributes(theme='dark')) + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', status_code=202, + json=dataclasses.asdict(exp)) + # test + client = RestClient(username="a", password="b") + response = client.update_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', firstname='Martin') + self.assertEqual(exp, response) + + def test_update_user_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', firstname='Martin') + except ForbiddenError as e: + pass + + def test_update_user_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', firstname='Martin') + except NotExistsError as e: + pass + + def test_update_user_foreign_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', status_code=405) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', firstname='Martin') + except ForbiddenError as e: + pass + + def test_update_user_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', status_code=405) + # test + try: + response = RestClient().update_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', firstname='Martin') + except AuthenticationError as e: + pass + + def test_update_user_theme_succeeds(self): + with requests_mock.Mocker() as mock: + exp = User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', given_name='Martin', + attributes=UserAttributes(theme='dark')) + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/theme', status_code=202, + json=dataclasses.asdict(exp)) + # test + client = RestClient(username="a", password="b") + response = client.update_user_theme(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', theme='dark') + self.assertEqual(exp, response) + + def test_update_user_theme_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/theme', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user_theme(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', theme='dark') + except ForbiddenError as e: + pass + + def test_update_user_theme_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/theme', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user_theme(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', theme='dark') + except NotExistsError as e: + pass + + def test_update_user_theme_foreign_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/theme', status_code=405) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user_theme(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', theme='dark') + except ResponseCodeError as e: + pass + + def test_update_user_theme_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/theme', status_code=405) + # test + try: + response = RestClient().update_user_theme(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', theme='dark') + except AuthenticationError as e: + pass + + def test_update_user_password_succeeds(self): + with requests_mock.Mocker() as mock: + exp = User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', given_name='Martin', + attributes=UserAttributes(theme='dark')) + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/password', status_code=202, + json=dataclasses.asdict(exp)) + # test + client = RestClient(username="a", password="b") + response = client.update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', + password='s3cr3t1n0rm4t10n') + self.assertEqual(exp, response) + + def test_update_user_password_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/password', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', + password='s3cr3t1n0rm4t10n') + except ForbiddenError as e: + pass + + def test_update_user_password_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/password', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', + password='s3cr3t1n0rm4t10n') + except NotExistsError as e: + pass + + def test_update_user_password_foreign_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/password', status_code=405) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', + password='s3cr3t1n0rm4t10n') + except ForbiddenError as e: + pass + + def test_update_user_password_keycloak_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/password', status_code=503) + # test + try: + client = RestClient(username="a", password="b") + response = client.update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', + password='s3cr3t1n0rm4t10n') + except ResponseCodeError as e: + pass + + def test_update_user_password_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/password', status_code=503) + # test + try: + response = RestClient().update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', + password='s3cr3t1n0rm4t10n') + except AuthenticationError as e: + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/python/tests/test_view.py b/lib/python/tests/test_view.py new file mode 100644 index 0000000000..e9a9ac5fde --- /dev/null +++ b/lib/python/tests/test_view.py @@ -0,0 +1,275 @@ +import unittest +import requests_mock +import dataclasses + +from dbrepo.RestClient import RestClient + +from dbrepo.api.dto import Table, UserAttributes, User, Column, Constraints, View, Result +from dbrepo.api.exceptions import ForbiddenError, NotExistsError, MalformedError, AuthenticationError + + +class ViewTest(unittest.TestCase): + + def test_get_views_empty_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/view', json=[]) + # test + response = RestClient().get_views(database_id=1) + self.assertEqual([], response) + + def test_get_views_succeeds(self): + with requests_mock.Mocker() as mock: + exp = [View(id=1, + name="Data", + internal_name="data", + database_id=1, + initial_view=False, + query="SELECT id FROM mytable WHERE deg > 0", + query_hash="94c74728b11a690e51d64719868824735f0817b7", + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + is_public=True, + created='2024-01-01 00:00:00', + last_modified='2024-01-01 00:00:00', + identifiers=[])] + # mock + mock.get('/api/database/1/view', json=[dataclasses.asdict(exp[0])]) + # test + response = RestClient().get_views(database_id=1) + self.assertEqual(exp, response) + + def test_get_views_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/view', status_code=403) + # test + try: + response = RestClient().get_views(database_id=1) + except ForbiddenError: + pass + + def test_get_views_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/view', status_code=404) + # test + try: + response = RestClient().get_views(database_id=1) + except NotExistsError: + pass + + def test_get_view_succeeds(self): + with requests_mock.Mocker() as mock: + exp = View(id=3, + name="Data", + internal_name="data", + database_id=1, + initial_view=False, + query="SELECT id FROM mytable WHERE deg > 0", + query_hash="94c74728b11a690e51d64719868824735f0817b7", + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + is_public=True, + created='2024-01-01 00:00:00', + last_modified='2024-01-01 00:00:00', + identifiers=[]) + # mock + mock.get('/api/database/1/view/3', json=dataclasses.asdict(exp)) + # test + response = RestClient().get_view(database_id=1, view_id=3) + self.assertEqual(exp, response) + + def test_get_view_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/view/3', status_code=403) + # test + try: + response = RestClient().get_view(database_id=1, view_id=3) + except ForbiddenError: + pass + + def test_get_views_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/view/3', status_code=404) + # test + try: + response = RestClient().get_view(database_id=1, view_id=3) + except NotExistsError: + pass + + def test_create_view_succeeds(self): + with requests_mock.Mocker() as mock: + exp = View(id=3, + name="Data", + internal_name="data", + database_id=1, + initial_view=False, + query="SELECT id FROM mytable WHERE deg > 0", + query_hash="94c74728b11a690e51d64719868824735f0817b7", + creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + is_public=True, + created='2024-01-01 00:00:00', + last_modified='2024-01-01 00:00:00', + identifiers=[]) + # mock + mock.post('/api/database/1/view', json=dataclasses.asdict(exp), status_code=201) + # test + client = RestClient(username="a", password="b") + response = client.create_view(database_id=1, name="Data", is_public=True, + query="SELECT id FROM mytable WHERE deg > 0") + self.assertEqual(exp, response) + + def test_create_view_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/view', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_view(database_id=1, name="Data", is_public=True, + query="SELECT id FROM mytable WHERE deg > 0") + except MalformedError: + pass + + def test_create_view_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/view', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_view(database_id=1, name="Data", is_public=True, + query="SELECT id FROM mytable WHERE deg > 0") + except ForbiddenError: + pass + + def test_create_view_not_found_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/view', status_code=404) + # test + try: + client = RestClient(username="a", password="b") + response = client.create_view(database_id=1, name="Data", is_public=True, + query="SELECT id FROM mytable WHERE deg > 0") + except NotExistsError: + pass + + def test_create_view_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.post('/api/database/1/view', status_code=404) + # test + try: + response = RestClient().create_view(database_id=1, name="Data", is_public=True, + query="SELECT id FROM mytable WHERE deg > 0") + except AuthenticationError: + pass + + def test_delete_view_succeeds(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/view/3', status_code=202) + # test + client = RestClient(username="a", password="b") + client.delete_view(database_id=1, view_id=3) + + def test_delete_view_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/view/3', status_code=400) + # test + try: + client = RestClient(username="a", password="b") + client.delete_view(database_id=1, view_id=3) + except MalformedError: + pass + + def test_delete_view_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/view/3', status_code=403) + # test + try: + client = RestClient(username="a", password="b") + client.delete_view(database_id=1, view_id=3) + except ForbiddenError: + pass + + def test_delete_view_not_auth_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.delete('/api/database/1/view/3', status_code=403) + # test + try: + RestClient().delete_view(database_id=1, view_id=3) + except AuthenticationError: + pass + + def test_get_view_data_succeeds(self): + with requests_mock.Mocker() as mock: + exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], + headers=[{'id': 0, 'username': 1}], + id=None) + # mock + mock.get('/api/database/1/view/3/data', json=dataclasses.asdict(exp)) + # test + response = RestClient().get_view_data(database_id=1, view_id=3) + self.assertEqual(exp, response) + + def test_get_view_data_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/view/3/data', status_code=400) + # test + try: + response = RestClient().get_view_data(database_id=1, view_id=3) + except MalformedError: + pass + + def test_get_view_data_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.get('/api/database/1/view/3/data', status_code=403) + # test + try: + response = RestClient().get_view_data(database_id=1, view_id=3) + except ForbiddenError: + pass + + def test_get_view_data_count_succeeds(self): + with requests_mock.Mocker() as mock: + exp = 844737 + # mock + mock.head('/api/database/1/view/3/data', headers={'X-Count': str(exp)}) + # test + response = RestClient().get_view_data_count(database_id=1, view_id=3) + self.assertEqual(exp, response) + + def test_get_view_data_count_malformed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/view/3/data', status_code=400) + # test + try: + response = RestClient().get_view_data_count(database_id=1, view_id=3) + except MalformedError: + pass + + def test_get_view_data_count_not_allowed_fails(self): + with requests_mock.Mocker() as mock: + # mock + mock.head('/api/database/1/view/3/data', status_code=403) + # test + try: + response = RestClient().get_view_data_count(database_id=1, view_id=3) + except ForbiddenError: + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/mkdocs.yml b/mkdocs.yml index 60f1c85e88..ae7381505a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,12 +31,9 @@ nav: - Search Database Dashboard: system-other-search-dashboard.md - Usage: - Overview: usage-overview.md + - Python Library: usage-python.md - Services: - - Analyse Service: usage-analyse.md - Authentication Service: usage-authentication.md - - Broker Service: usage-broker.md - - Metadata Service: usage-metadata.md - - Search Service: usage-search.md - Storage Service: usage-storage.md - Upload Service: usage-upload.md - publications.md @@ -59,6 +56,7 @@ theme: - navigation.sections - content.code.annotate - content.code.copy + - content.tooltips icon: repo: fontawesome/brands/git-alt palette: -- GitLab