From c8d7f1d9571663fb3859f5040456c23ccc4402d4 Mon Sep 17 00:00:00 2001 From: tonurmi Date: Tue, 1 Feb 2022 14:22:38 +0200 Subject: [PATCH] Update to version 2.7.0 --- .gitlab-ci.yml | 14 +- ENV_VARS.md | 1 + docker-compose.yml | 2 + poetry.lock | 713 ++++++++++-------- pyproject.toml | 3 +- requirements.txt | 92 ++- src/metax_api/api/rest/base/router.py | 6 + .../api/rest/base/serializers/__init__.py | 1 + .../serializers/data_catalog_serializer.py | 2 + .../editor_permissions_serializer.py | 20 + src/metax_api/api/rest/base/views/__init__.py | 1 + .../api/rest/base/views/common_view.py | 8 +- .../api/rest/base/views/dataset_view.py | 13 +- .../api/rest/base/views/directory_view.py | 2 +- .../base/views/editor_permissions_view.py | 144 ++++ src/metax_api/api/rest/v2/router.py | 6 + .../api/rest/v2/views/api_error_view.py | 2 +- .../api/rest/v2/views/dataset_view.py | 2 +- .../api/rpc/base/views/dataset_rpc.py | 42 ++ src/metax_api/api/rpc/base/views/file_rpc.py | 5 +- .../api/rpc/base/views/statistic_rpc.py | 5 + src/metax_api/initialdata/datacatalogs.json | 288 ++++++- .../management/commands/mark_files_removed.py | 3 + src/metax_api/middleware/identifyapicaller.py | 12 +- .../middleware/stream_http_response.py | 5 +- .../migrations/0041_add_editorpermissions.py | 195 +++++ .../migrations/0042_auto_20211108_1256.py | 21 + ...43_remove_editoruserpermission_verified.py | 17 + .../0044_change_aalto_catalog_to_harvested.py | 34 + .../0045_add_publish_fields_to_catalogs.py | 50 ++ src/metax_api/models/__init__.py | 2 +- src/metax_api/models/catalog_record.py | 160 +++- src/metax_api/models/catalog_record_v2.py | 6 + src/metax_api/models/data_catalog.py | 7 + src/metax_api/models/directory.py | 108 +-- .../services/catalog_record_service.py | 20 + src/metax_api/services/file_service.py | 10 +- src/metax_api/services/rabbitmq_service.py | 5 +- src/metax_api/services/rems_service.py | 33 +- src/metax_api/services/statistic_service.py | 61 +- .../settings/components/access_control.py | 34 + src/metax_api/settings/components/common.py | 16 +- src/metax_api/settings/components/logging.py | 2 +- src/metax_api/settings/components/rems.py | 1 + .../settings/environments/production.py | 2 + src/metax_api/settings/environments/stable.py | 6 +- .../settings/environments/staging.py | 1 + .../settings/environments/unittests.py | 16 + src/metax_api/swagger/v1/swagger.yaml | 211 +++++- src/metax_api/swagger/v2/swagger.yaml | 205 ++++- .../tests/api/rest/base/views/common/read.py | 13 +- .../api/rest/base/views/datasets/read.py | 21 +- .../api/rest/base/views/datasets/write.py | 81 ++ .../base/views/editorpermissions/__init__.py | 2 + .../rest/base/views/editorpermissions/read.py | 110 +++ .../base/views/editorpermissions/write.py | 219 ++++++ .../tests/api/rest/base/views/schemas/read.py | 2 +- .../api/rest/v2/views/datasets/drafts.py | 16 + .../tests/api/rest/v2/views/datasets/write.py | 72 +- .../tests/api/rpc/base/views/dataset_rpc.py | 95 +++ .../tests/api/rpc/base/views/file_rpc.py | 71 +- .../tests/api/rpc/base/views/statistic_rpc.py | 58 ++ .../tests/api/rpc/v2/views/dataset_rpc.py | 15 + .../data_catalog_test_data_template.json | 6 +- .../tests/testdata/generate_test_data.py | 88 ++- src/metax_api/tests/testdata/test_data.json | 466 +++++++++++- 66 files changed, 3343 insertions(+), 607 deletions(-) create mode 100644 src/metax_api/api/rest/base/serializers/editor_permissions_serializer.py create mode 100644 src/metax_api/api/rest/base/views/editor_permissions_view.py create mode 100644 src/metax_api/migrations/0041_add_editorpermissions.py create mode 100644 src/metax_api/migrations/0042_auto_20211108_1256.py create mode 100644 src/metax_api/migrations/0043_remove_editoruserpermission_verified.py create mode 100755 src/metax_api/migrations/0044_change_aalto_catalog_to_harvested.py create mode 100644 src/metax_api/migrations/0045_add_publish_fields_to_catalogs.py create mode 100644 src/metax_api/tests/api/rest/base/views/editorpermissions/__init__.py create mode 100644 src/metax_api/tests/api/rest/base/views/editorpermissions/read.py create mode 100644 src/metax_api/tests/api/rest/base/views/editorpermissions/write.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 02300332..87e647dd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,13 +15,13 @@ deploy: stage: deploy environment: $CI_COMMIT_REF_NAME script: - - ansible-playbook -i $ANSIBLE_INVENTORY $DEPLOY_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA repo_version=$CI_COMMIT_REF_NAME" + - ansible-playbook -i $ANSIBLE_INVENTORY $DEPLOY_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" integration_test: stage: test environment: $CI_COMMIT_REF_NAME script: - - ansible-playbook -i $ANSIBLE_INVENTORY $TEST_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" + - ansible-playbook -i $ANSIBLE_INVENTORY $TEST_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" clean_test: stage: clean_test @@ -29,7 +29,7 @@ clean_test: name: $CI_COMMIT_REF_NAME on_stop: clean_gitlab_env script: - - ansible-playbook -i $ANSIBLE_INVENTORY $DELETE_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" + - ansible-playbook -i $ANSIBLE_INVENTORY $DELETE_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: always @@ -53,8 +53,8 @@ update_metax: stage: update environment: $CI_COMMIT_REF_NAME script: - - ansible-playbook -i $ANSIBLE_INVENTORY $UPDATE_PROXY_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" - - ansible-playbook -i $ANSIBLE_INVENTORY $MANAGE_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" + - ansible-playbook -i $ANSIBLE_INVENTORY $UPDATE_PROXY_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" + - ansible-playbook -i $ANSIBLE_INVENTORY $MANAGE_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" rules: - if: $CI_COMMIT_BRANCH =~ /^(demo|stable|staging|test)$/ when: always @@ -64,8 +64,8 @@ clean_previous_build: stage: clean_build environment: $CI_COMMIT_REF_NAME script: - - ansible-playbook -i $ANSIBLE_INVENTORY $DELETE_PLAYBOOK -e "build_id=${CI_COMMIT_BEFORE_SHA:0:8}" + - ansible-playbook -i $ANSIBLE_INVENTORY $DELETE_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" rules: - if: $CI_COMMIT_BRANCH =~ /^(demo|stable|staging|test)$/ - when: always + when: manual - when: never diff --git a/ENV_VARS.md b/ENV_VARS.md index 7afadd56..ec31024b 100755 --- a/ENV_VARS.md +++ b/ENV_VARS.md @@ -56,6 +56,7 @@ copy .env.template to .env and fill the required values from below table. Requir | REMS_FORM_ID | no | | Required if REMS is enabled | | REMS_METAX_USER | no | | Required if REMS is enabled | | REMS_REPORTER_USER | no | | Required if REMS is enabled | +| REMS_ORGANIZATION | no | | Required if REMS is enabled | | SERVER_DOMAIN_NAME | no | metax.fd-dev.csc.fi | | ENABLE_V1_ENDPOINTS | no | True | ENABLE_V2_ENDPOINTS | no | True diff --git a/docker-compose.yml b/docker-compose.yml index 7e37522f..f4562610 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,8 @@ services: metax-rabbitmq: image: rabbitmq:3-management hostname: metax-rabbitmq + ports: + - 8050:15672 volumes: - metax-rabbitmq:/var/lib/rabbitmq diff --git a/poetry.lock b/poetry.lock index 05e4a555..8be0ea6c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -118,23 +118,26 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "certifi" -version = "2021.5.30" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = "*" [[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" +name = "charset-normalizer" +version = "2.0.9" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.1" +version = "8.0.3" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -187,7 +190,7 @@ tests = ["responses (>=0.10.6)", "mock (>=1.3.0)", "pytest-invenio (>=1.4.0)"] [[package]] name = "decorator" -version = "5.0.9" +version = "5.1.0" description = "Decorators for Humans" category = "dev" optional = false @@ -195,24 +198,24 @@ python-versions = ">=3.5" [[package]] name = "django" -version = "3.1.13" +version = "3.2.10" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -asgiref = ">=3.2.10,<4" +asgiref = ">=3.3.2,<4" pytz = "*" sqlparse = ">=0.2.2" [package.extras] -argon2 = ["argon2-cffi (>=16.1.0)"] +argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "django-debug-toolbar" -version = "3.2.1" +version = "3.2.3" description = "A configurable set of panels that display various debug information about the current request/response." category = "dev" optional = false @@ -230,20 +233,9 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "django-rainbowtests" -version = "0.6.0" -description = "A colorful Django Test Runner." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -django = "*" - [[package]] name = "django-split-settings" -version = "1.0.1" +version = "1.1.0" description = "Organize Django settings into multiple files and directories. Easily override and modify settings. Use wildcards and optional settings files." category = "main" optional = false @@ -262,14 +254,15 @@ django = ">=2.0" [[package]] name = "djangorestframework" -version = "3.12.4" +version = "3.13.0" description = "Web APIs for Django, made easy." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] django = ">=2.2" +pytz = "*" [[package]] name = "docutils" @@ -298,7 +291,7 @@ https = ["urllib3[secure] (>=1.24.1)"] [[package]] name = "elasticsearch" -version = "7.13.3" +version = "7.16.1" description = "Python client for Elasticsearch" category = "main" optional = false @@ -316,7 +309,7 @@ requests = ["requests (>=2.4.0,<3.0.0)"] [[package]] name = "executing" -version = "0.7.0" +version = "0.8.2" description = "Get the currently executing AST node of a frame, and other information" category = "dev" optional = false @@ -352,15 +345,15 @@ pygments = ">=2.2.0" [[package]] name = "idna" -version = "2.10" +version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" [[package]] name = "idutils" -version = "1.1.8" +version = "1.1.9" description = "Small library for persistent identifiers used in scholarly communication." category = "main" optional = false @@ -371,13 +364,13 @@ isbnid-fork = ">=0.4.4" six = ">=1.10" [package.extras] -all = ["Sphinx (>=1.4.2)", "check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest-runner (>=2.6.2)", "pytest (>=3.6.0)"] -docs = ["Sphinx (>=1.4.2)"] -tests = ["check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest-runner (>=2.6.2)", "pytest (>=3.6.0)"] +all = ["Sphinx (>=3)", "pytest-cache (>=1.0)", "pytest-runner (>=2.6.2)", "pytest-invenio (>=1.4.0)"] +docs = ["Sphinx (>=3)"] +tests = ["pytest-cache (>=1.0)", "pytest-runner (>=2.6.2)", "pytest-invenio (>=1.4.0)"] [[package]] name = "imagesize" -version = "1.2.0" +version = "1.3.0" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "main" optional = true @@ -385,7 +378,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.6.1" +version = "4.8.2" description = "Read metadata from Python packages" category = "main" optional = false @@ -398,7 +391,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "ipdb" @@ -415,7 +408,7 @@ toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} [[package]] name = "ipython" -version = "7.25.0" +version = "7.30.1" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -445,14 +438,6 @@ parallel = ["ipyparallel"] qtconsole = ["qtconsole"] test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] -[[package]] -name = "ipython-genutils" -version = "0.2.0" -description = "Vestigial utilities from IPython" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "isbnid-fork" version = "0.5.2" @@ -469,7 +454,7 @@ tests = ["pytest-pep8 (>=1.0.6)", "pytest (>=3.0.4)"] [[package]] name = "isodate" -version = "0.6.0" +version = "0.6.1" description = "An ISO 8601 date/time/duration parser and formatter" category = "main" optional = false @@ -480,7 +465,7 @@ six = "*" [[package]] name = "isort" -version = "5.9.1" +version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -494,7 +479,7 @@ plugins = ["setuptools"] [[package]] name = "jedi" -version = "0.18.0" +version = "0.18.1" description = "An autocompletion tool for Python that can be used for text editors." category = "dev" optional = false @@ -505,11 +490,11 @@ parso = ">=0.8.0,<0.9.0" [package.extras] qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.0.1" +version = "3.0.3" description = "A very fast and expressive template engine." category = "main" optional = true @@ -553,7 +538,7 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} [[package]] name = "lxml" -version = "4.6.3" +version = "4.7.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -575,7 +560,7 @@ python-versions = ">=3.6" [[package]] name = "matplotlib-inline" -version = "0.1.2" +version = "0.1.3" description = "Inline Matplotlib backend for Jupyter" category = "dev" optional = false @@ -594,18 +579,18 @@ python-versions = "*" [[package]] name = "packaging" -version = "21.0" +version = "21.3" description = "Core utilities for Python packages" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "parso" -version = "0.8.2" +version = "0.8.3" description = "A Python Parser" category = "dev" optional = false @@ -617,11 +602,11 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pathspec" -version = "0.8.1" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pexpect" @@ -657,18 +642,18 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.19" +version = "3.0.24" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.6.2" [package.dependencies] wcwidth = "*" [[package]] name = "psycopg2-binary" -version = "2.9.1" +version = "2.9.2" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false @@ -684,7 +669,7 @@ python-versions = "*" [[package]] name = "pygments" -version = "2.9.0" +version = "2.10.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -692,15 +677,15 @@ python-versions = ">=3.5" [[package]] name = "pyjwt" -version = "2.1.0" +version = "2.3.0" description = "JSON Web Token implementation in Python" category = "dev" optional = false python-versions = ">=3.6" [package.extras] -crypto = ["cryptography (>=3.3.1,<4.0.0)"] -dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] +crypto = ["cryptography (>=3.3.1)"] +dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] @@ -725,11 +710,14 @@ resolved_reference = "5f6eba1201270d930ed684e15e9b9fc885649d17" [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.6" description = "Python parsing module" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyrsistent" @@ -741,7 +729,7 @@ python-versions = ">=3.6" [[package]] name = "python-box" -version = "5.3.0" +version = "5.4.1" description = "Advanced Python dictionaries with dot notation access" category = "main" optional = false @@ -753,10 +741,11 @@ all = ["ruamel.yaml", "toml", "msgpack"] msgpack = ["msgpack"] "ruamel.yaml" = ["ruamel.yaml"] toml = ["toml"] +yaml = ["ruamel.yaml"] [[package]] name = "python-dateutil" -version = "2.8.1" +version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "main" optional = false @@ -775,7 +764,7 @@ python-versions = "*" [[package]] name = "pytz" -version = "2021.1" +version = "2021.3" description = "World timezone definitions, modern and historical" category = "main" optional = false @@ -821,7 +810,7 @@ hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "regex" -version = "2021.7.6" +version = "2021.11.10" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -829,25 +818,25 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.1" +version = "2.26.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "responses" -version = "0.13.3" +version = "0.13.4" description = "A utility library for mocking out the `requests` Python library." category = "dev" optional = false @@ -859,7 +848,7 @@ six = "*" urllib3 = ">=1.25.10" [package.extras] -tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] +tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "types-mock", "types-requests", "types-six", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] [[package]] name = "six" @@ -871,7 +860,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "snowballstemmer" -version = "2.1.0" +version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "main" optional = true @@ -879,7 +868,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.0.3" +version = "4.3.1" description = "Python documentation generator" category = "main" optional = true @@ -898,10 +887,10 @@ requests = ">=2.5.0" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] @@ -1012,7 +1001,7 @@ test = ["pytest"] [[package]] name = "sqlparse" -version = "0.4.1" +version = "0.4.2" description = "A non-validating SQL parser." category = "main" optional = false @@ -1020,7 +1009,7 @@ python-versions = ">=3.5" [[package]] name = "structlog" -version = "21.1.0" +version = "21.4.0" description = "Structured Logging for Python" category = "main" optional = false @@ -1030,9 +1019,9 @@ python-versions = ">=3.6" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -dev = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-toolbox", "twisted", "pre-commit"] -docs = ["furo", "sphinx", "sphinx-toolbox", "twisted"] -tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson"] +dev = ["pre-commit", "rich", "cogapp", "tomli", "coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson"] [[package]] name = "tblib" @@ -1060,37 +1049,34 @@ python-versions = ">= 3.5" [[package]] name = "traitlets" -version = "5.0.5" +version = "5.1.1" description = "Traitlets Python configuration system" category = "dev" optional = false python-versions = ">=3.7" -[package.dependencies] -ipython-genutils = "*" - [package.extras] test = ["pytest"] [[package]] name = "typed-ast" -version = "1.4.3" +version = "1.5.1" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "3.10.0.0" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.6" +version = "1.26.7" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1119,7 +1105,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "zipp" -version = "3.5.0" +version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1137,7 +1123,7 @@ swagger = ["PyYAML"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "f902a1619b9a3a0cdc7233bee55b16b6b3167d7ce087716503df3c9bd39fe484" +content-hash = "c0f6e79d018c633ee4c3b935d5253e27d9e155e0f2b5a33f0e9d0a67b62227a3" [metadata.files] alabaster = [ @@ -1179,16 +1165,16 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] certifi = [ - {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, - {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +charset-normalizer = [ + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1253,35 +1239,32 @@ datacite = [ {file = "datacite-1.1.2.tar.gz", hash = "sha256:0164bc2ff35bba643897201eb359611abb43ff5811a9ac17fb5592cd643b4443"}, ] decorator = [ - {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, - {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, + {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, + {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, ] django = [ - {file = "Django-3.1.13-py3-none-any.whl", hash = "sha256:a6e0d1ff11095b7394c079ade7094c73b2dc3df4a7a373c9b58ed73b77a97feb"}, - {file = "Django-3.1.13.tar.gz", hash = "sha256:9f8be75646f62204320b195062b1d696ba28aa3d45ee72fb7c888ffaebc5bdb2"}, + {file = "Django-3.2.10-py3-none-any.whl", hash = "sha256:df6f5eb3c797b27c096d61494507b7634526d4ce8d7c8ca1e57a4fb19c0738a3"}, + {file = "Django-3.2.10.tar.gz", hash = "sha256:074e8818b4b40acdc2369e67dcd6555d558329785408dcd25340ee98f1f1d5c4"}, ] django-debug-toolbar = [ - {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, - {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"}, + {file = "django-debug-toolbar-3.2.3.tar.gz", hash = "sha256:95880677ea846ba1077d02305fd5e2b25e1da096e1d4a735b665e3340fa2ae79"}, + {file = "django_debug_toolbar-3.2.3-py3-none-any.whl", hash = "sha256:516702e1d71302bbc06059fa3c41efd1a3bd9cbb5580fc793343118d95b309e0"}, ] django-environ = [ {file = "django-environ-0.4.5.tar.gz", hash = "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde"}, {file = "django_environ-0.4.5-py2.py3-none-any.whl", hash = "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4"}, ] -django-rainbowtests = [ - {file = "django-rainbowtests-0.6.0.tar.gz", hash = "sha256:0700ee1386935822dca296d323d67b0563cb2e5012b553ebca7c9391f2298cd9"}, -] django-split-settings = [ - {file = "django-split-settings-1.0.1.tar.gz", hash = "sha256:2da16cd967cd38315ec7ff0ae0c9db8488f8528bb2e5de26cd898328dc4bbeac"}, - {file = "django_split_settings-1.0.1-py3-none-any.whl", hash = "sha256:8d636649023289d0ef0ba08b0a4f37761adc94a29ee0ebfe65922c3cb0594ede"}, + {file = "django-split-settings-1.1.0.tar.gz", hash = "sha256:6b3aed89667a95525152026eab93a9f038ff22df6883006318b8b4a3d0ca6888"}, + {file = "django_split_settings-1.1.0-py3-none-any.whl", hash = "sha256:5d97ae64cf9ed14a831722d82ac725944667ac8c08307b7cfd22e91367b411d0"}, ] django-watchman = [ {file = "django-watchman-1.2.0.tar.gz", hash = "sha256:c38830c58984b8eb29db30a3e332968d1c7e235dee3f5c0a907d8d79a37a3125"}, {file = "django_watchman-1.2.0-py2.py3-none-any.whl", hash = "sha256:6c0b8889456ed644fcfe2f7c3294cb4ef8568a85772b7f95842e2d8c9b4bbff5"}, ] djangorestframework = [ - {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"}, - {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"}, + {file = "djangorestframework-3.13.0-py3-none-any.whl", hash = "sha256:48e64f08244fa0df9e2b8fbd405edec263d8e1251112a06d0073b546b7c86b9c"}, + {file = "djangorestframework-3.13.0.tar.gz", hash = "sha256:8b987d5683f5b3553dd946d4972048d3117fc526cb0bc01a3f021e81af53f39e"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -1297,14 +1280,15 @@ dulwich = [ {file = "dulwich-0.19.16.tar.gz", hash = "sha256:f74561c448bfb6f04c07de731c1181ae4280017f759b0bb04fa5770aa84ca850"}, ] elasticsearch = [ - {file = "elasticsearch-7.13.3-py2.py3-none-any.whl", hash = "sha256:76cc7670449676138acbab8872eb34867db033701310d9b01fb2ecfba5fb7234"}, - {file = "elasticsearch-7.13.3.tar.gz", hash = "sha256:d539d82552804b3d41033377b9adf11c39c749ee1764af3d74b56f42177e3281"}, + {file = "elasticsearch-7.16.1-py2.py3-none-any.whl", hash = "sha256:97e9d62db71a571c540d6dfa2a51bc2263b5c0d7b9f9896eb7116ba02373c69b"}, + {file = "elasticsearch-7.16.1.tar.gz", hash = "sha256:c024ee2e7e2509c842c4e3c5e2b99a92ceecfde06d6dac2d32a19bf566c3e175"}, ] executing = [ - {file = "executing-0.7.0-py2.py3-none-any.whl", hash = "sha256:1971c98963857f2c03f4b688d93fc4b28ce756bd102955ea8ea7ce0a7fd9a28f"}, - {file = "executing-0.7.0.tar.gz", hash = "sha256:509fe590e9da1c0659a273c42493a25af6f43d61cf36f085fc1b6cf2c6419d1f"}, + {file = "executing-0.8.2-py2.py3-none-any.whl", hash = "sha256:32fc6077b103bd19e6494a72682d66d5763cf20a106d5aa7c5ccbea4e47b0df7"}, + {file = "executing-0.8.2.tar.gz", hash = "sha256:c23bf42e9a7b9b212f185b1b2c3c91feb895963378887bb10e64a2e612ec0023"}, ] gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] icecream = [ @@ -1312,50 +1296,46 @@ icecream = [ {file = "icecream-2.1.1.tar.gz", hash = "sha256:47e00e3f4e8477996e7dc420b6fa8ba53f8ced17de65320fedb5b15997b76589"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] idutils = [ - {file = "IDUtils-1.1.8-py2.py3-none-any.whl", hash = "sha256:6a032009da9dad262c2d1ad4978100b5937749e27862845a7bf49c38a39e3fff"}, - {file = "IDUtils-1.1.8.tar.gz", hash = "sha256:21497bf279b64aadce923a11b1ed9b601c0bf01eb82c3f952877eef026586d78"}, + {file = "IDUtils-1.1.9-py2.py3-none-any.whl", hash = "sha256:01edcde8394b73dc725958770e6e8d20088790345639982698715749f8213acc"}, + {file = "IDUtils-1.1.9.tar.gz", hash = "sha256:80c10f2ecb5cf020aa788ea860fa1f450a5a6714a0e95ae62334d5d286b395be"}, ] imagesize = [ - {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, - {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, + {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, + {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, - {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, + {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, + {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, ] ipdb = [ {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, ] ipython = [ - {file = "ipython-7.25.0-py3-none-any.whl", hash = "sha256:aa21412f2b04ad1a652e30564fff6b4de04726ce875eab222c8430edc6db383a"}, - {file = "ipython-7.25.0.tar.gz", hash = "sha256:54bbd1fe3882457aaf28ae060a5ccdef97f212a741754e420028d4ec5c2291dc"}, -] -ipython-genutils = [ - {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, - {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, + {file = "ipython-7.30.1-py3-none-any.whl", hash = "sha256:fc60ef843e0863dd4e24ab2bb5698f071031332801ecf8d1aeb4fb622056545c"}, + {file = "ipython-7.30.1.tar.gz", hash = "sha256:cb6aef731bf708a7727ab6cde8df87f0281b1427d41e65d62d4b68934fa54e97"}, ] isbnid-fork = [ {file = "isbnid_fork-0.5.2.tar.gz", hash = "sha256:8d878866aa0e7f06e700a37fce586c7398ce4837da8bca39683db7028a9c3837"}, ] isodate = [ - {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, - {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, ] isort = [ - {file = "isort-5.9.1-py3-none-any.whl", hash = "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"}, - {file = "isort-5.9.1.tar.gz", hash = "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56"}, + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jedi = [ - {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, - {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, + {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, + {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, ] jinja2 = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, + {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, + {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, ] jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, @@ -1365,50 +1345,90 @@ livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] lxml = [ - {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, - {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"}, - {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"}, - {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"}, - {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"}, - {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"}, - {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"}, - {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"}, - {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"}, - {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"}, - {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"}, - {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"}, - {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"}, - {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"}, - {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"}, - {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"}, - {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"}, - {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"}, - {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"}, - {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"}, - {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"}, - {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"}, + {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, + {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, + {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, + {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, + {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, + {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, + {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, + {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, + {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, + {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, + {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, + {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, + {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, + {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, + {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, + {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, + {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, + {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, + {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, + {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, + {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, + {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, + {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, + {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, + {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, + {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1417,14 +1437,27 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1434,29 +1467,35 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] matplotlib-inline = [ - {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"}, - {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"}, + {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, + {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] parso = [ - {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, - {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, ] pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pexpect = [ {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, @@ -1471,56 +1510,63 @@ pika = [ {file = "pika-1.2.0.tar.gz", hash = "sha256:f023d6ac581086b124190cb3dc81dd581a149d216fa4540ac34f9be1e3970b89"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, - {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, + {file = "prompt_toolkit-3.0.24-py3-none-any.whl", hash = "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"}, + {file = "prompt_toolkit-3.0.24.tar.gz", hash = "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6"}, ] psycopg2-binary = [ - {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-win32.whl", hash = "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-win32.whl", hash = "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68"}, + {file = "psycopg2-binary-2.9.2.tar.gz", hash = "sha256:234b1f48488b2f86aac04fb00cb04e5e9bcb960f34fa8a8e41b73149d581a93b"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c0e1fb7097ded2cc44d9037cfc68ad86a30341261492e7de95d180e534969fb2"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:717525cdc97b23182ff6f470fb5bf6f0bc796b5a7000c6f6699d6679991e4a5e"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3865d0cd919349c45603bd7e80249a382c5ecf8106304cfd153282adf9684b6a"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:daf6b5c62eb738872d61a1fa740d7768904911ba5a7e055ed72169d379b58beb"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:3ac83656ff4fbe7f2a956ab085e3eb1d678df54759965d509bdd6a06ce520d49"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-win32.whl", hash = "sha256:a04cfa231e7d9b63639e62166a4051cb47ca599fa341463fa3e1c48585fcee64"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:c6e16e085fe6dc6c099ee0be56657aa9ad71027465ef9591d302ba230c404c7e"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:53912199abb626a7249c662e72b70b4f57bf37f840599cec68625171435790dd"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:029e09a892b9ebc3c77851f69ce0720e1b72a9c6850460cee49b14dfbf9ccdd2"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db1b03c189f85b8df29030ad32d521dd7dcb862fd5f8892035314f5b886e70ce"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:2eecbdc5fa5886f2dd6cc673ce4291cc0fb8900965315268960ad9c2477f8276"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:a77e98c68b0e6c51d4d6a994d22b30e77276cbd33e4aabdde03b9ad3a2c148aa"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:bf31e6fdb4ec1f6d98a07f48836508ed6edd19b48b13bbf168fbc1bd014b4ca2"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f9c37ecb173d76cf49e519133fd70851b8f9c38b6b8c1cb7fcfc71368d4cc6fc"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a507db7758953b1b170c4310691a1a89877029b1e11b08ba5fc8ae3ddb35596b"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e4bbcfb403221ea1953f3e0a85cef00ed15c1683a66cf35c956a7e37c33a4c4"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4dff0f15af6936c6fe6da7067b4216edbbe076ad8625da819cc066591b1133c"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:8d2aafe46eb87742425ece38130510fbb035787ee89a329af299029c4d9ae318"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:37c8f00f7a2860bac9f7a54f03c243fc1dd9b367e5b2b52f5a02e5f4e9d8c49b"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:ef97578fab5115e3af4334dd3376dea3c3a79328a3314b21ec7ced02920b916d"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7e6bd4f532c2cd297b81114526176b240109a1c52020adca69c3f3226c65dc18"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:eeee7b18c51d02e49bf1984d7af26e8843fe68e31fa1cbab5366ebdfa1c89ade"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:497372cc76e6cbce2f51b37be141f360a321423c03eb9be45524b1d123f4cd11"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671699aff57d22a245b7f4bba89e3de97dc841c5e98bd7f685429b2b20eca47"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:b9d45374ba98c1184df9cce93a0b766097544f8bdfcd5de83ff10f939c193125"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:a1852c5bef7e5f52bd43fde5eda610d4df0fb2efc31028150933e84b4140d47a"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-win32.whl", hash = "sha256:108b0380969ddab7c8ef2a813a57f87b308b2f88ec15f1a1e7b653964a3cfb25"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:14427437117f38e65f71db65d8eafd0e86837be456567798712b8da89db2b2dd"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:578c279cd1ce04f05ae0912530ece00bab92854911808e5aec27588aba87e361"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2dea4deac3dd3687e32daeb0712ee96c535970dfdded37a11de6a21145ab0e"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b592f09ff18cfcc9037b9a976fcd62db48cae9dbd5385f2471d4c2ba40c52b4d"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:3a320e7a804f3886a599fea507364aaafbb8387027fffcdfbd34d96316c806c7"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7585ca73dcfe326f31fafa8f96e6bb98ea9e9e46c7a1924ec8101d797914ae27"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-win32.whl", hash = "sha256:9c0aaad07941419926b9bd00171e49fe6b06e42e5527fb91671e137fe6c93d77"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa2847d8073951dbc84c4f8b32c620764db3c2eb0d99a04835fecfab7d04816e"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] pygments = [ - {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, - {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pyjwt = [ - {file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"}, - {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"}, + {file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"}, + {file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"}, ] pyoai = [] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pyrsistent = [ {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, @@ -1546,12 +1592,12 @@ pyrsistent = [ {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, ] python-box = [ - {file = "python-box-5.3.0.tar.gz", hash = "sha256:4ed4ef5d34de505a65c01e3f1911de8cdb29484fcae0c035141dce535c6c194a"}, - {file = "python_box-5.3.0-py3-none-any.whl", hash = "sha256:f2a531f9f5bbef078c175fad6abb31e9b59d40d121ea79993197e6bb221c6be6"}, + {file = "python-box-5.4.1.tar.gz", hash = "sha256:b68e0f8abc86f3deda751b3390f64df64a0989459de51ba4db949662a7b4d8ac"}, + {file = "python_box-5.4.1-py3-none-any.whl", hash = "sha256:60ae9156de34cf92b899bd099580950df70a5b0813e67a3310a1cdd1976457fa"}, ] python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-simplexquery = [ {file = "python-simplexquery-1.0.5.3.win32-py2.6.exe", hash = "sha256:a32281dd8a923930c177c7a5b6124e4f358c02773e40e6e734a1217fb9a5ab86"}, @@ -1561,8 +1607,8 @@ python-simplexquery = [ {file = "python-simplexquery-1.0.5.3.zip", hash = "sha256:4849070678538d26778c9902c58eac13a88beaffc526e69ee5e3db3744499a2b"}, ] pytz = [ - {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, - {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -1604,67 +1650,100 @@ redis = [ {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] regex = [ - {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, - {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, - {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, - {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, - {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, - {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, - {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, - {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, - {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, - {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, - {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, - {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, - {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, + {file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"}, + {file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b"}, + {file = "regex-2021.11.10-cp310-cp310-win32.whl", hash = "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a"}, + {file = "regex-2021.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12"}, + {file = "regex-2021.11.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00"}, + {file = "regex-2021.11.10-cp36-cp36m-win32.whl", hash = "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4"}, + {file = "regex-2021.11.10-cp36-cp36m-win_amd64.whl", hash = "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e"}, + {file = "regex-2021.11.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a"}, + {file = "regex-2021.11.10-cp37-cp37m-win32.whl", hash = "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec"}, + {file = "regex-2021.11.10-cp37-cp37m-win_amd64.whl", hash = "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4"}, + {file = "regex-2021.11.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83"}, + {file = "regex-2021.11.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0"}, + {file = "regex-2021.11.10-cp38-cp38-win32.whl", hash = "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc"}, + {file = "regex-2021.11.10-cp38-cp38-win_amd64.whl", hash = "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d"}, + {file = "regex-2021.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b"}, + {file = "regex-2021.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d"}, + {file = "regex-2021.11.10-cp39-cp39-win32.whl", hash = "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a"}, + {file = "regex-2021.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29"}, + {file = "regex-2021.11.10.tar.gz", hash = "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] responses = [ - {file = "responses-0.13.3-py2.py3-none-any.whl", hash = "sha256:b54067596f331786f5ed094ff21e8d79e6a1c68ef625180a7d34808d6f36c11b"}, - {file = "responses-0.13.3.tar.gz", hash = "sha256:18a5b88eb24143adbf2b4100f328a2f5bfa72fbdacf12d97d41f07c26c45553d"}, + {file = "responses-0.13.4-py2.py3-none-any.whl", hash = "sha256:d8d0f655710c46fd3513b9202a7f0dcedd02ca0f8cf4976f27fa8ab5b81e656d"}, + {file = "responses-0.13.4.tar.gz", hash = "sha256:9476775d856d3c24ae660bbebe29fb6d789d4ad16acd723efbfb6ee20990b899"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snowballstemmer = [ - {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, - {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sphinx = [ - {file = "Sphinx-4.0.3-py3-none-any.whl", hash = "sha256:5747f3c855028076fcff1e4df5e75e07c836f0ac11f7df886747231092cfe4ad"}, - {file = "Sphinx-4.0.3.tar.gz", hash = "sha256:dff357e6a208eb7edb2002714733ac21a9fe597e73609ff417ab8cf0c6b4fbb8"}, + {file = "Sphinx-4.3.1-py3-none-any.whl", hash = "sha256:048dac56039a5713f47a554589dc98a442b39226a2b9ed7f82797fcb2fe9253f"}, + {file = "Sphinx-4.3.1.tar.gz", hash = "sha256:32a5b3e9a1b176cc25ed048557d4d3d01af635e6b76c5bc7a43b0a34447fbd45"}, ] sphinx-autobuild = [ {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, @@ -1699,12 +1778,12 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] sqlparse = [ - {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, - {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, + {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, + {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, ] structlog = [ - {file = "structlog-21.1.0-py2.py3-none-any.whl", hash = "sha256:62f06fc0ee32fb8580f0715eea66cb87271eb7efb0eaf9af6b639cba8981de47"}, - {file = "structlog-21.1.0.tar.gz", hash = "sha256:d9d2d890532e8db83c6977a2a676fb1889922ff0c26ad4dc0ecac26f9fafbc57"}, + {file = "structlog-21.4.0-py3-none-any.whl", hash = "sha256:6ed8fadb27cf8362be0e606f5e79ccdd3b1e879aac65f9dc0ac3033fd013a7be"}, + {file = "structlog-21.4.0.tar.gz", hash = "sha256:305a66201f9605a2e8a2595271a446f258175901c09c01e4c2c2a8ac5b68edf1"}, ] tblib = [ {file = "tblib-1.7.0-py2.py3-none-any.whl", hash = "sha256:289fa7359e580950e7d9743eab36b0691f0310fce64dee7d9c31065b8f723e23"}, @@ -1758,49 +1837,37 @@ tornado = [ {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] traitlets = [ - {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, - {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, + {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"}, + {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, ] typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, + {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, + {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, + {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, + {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, + {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, + {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, + {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, + {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, + {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, + {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, + {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, + {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, + {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ - {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, - {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -1811,6 +1878,6 @@ xmltodict = [ {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, ] zipp = [ - {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, - {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] diff --git a/pyproject.toml b/pyproject.toml index 8742b747..7a0af20b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.7" -Django = "<3.2" +Django = "<3.3" datacite = "^1.0.1" djangorestframework = "^3.12.4" gunicorn = "^20.1.0" @@ -37,7 +37,6 @@ django-watchman = "^1.2.0" icecream = "^2.1.0" black = {version = "^20.8b1", allow-prereleases = true} tblib = "^1.7.0" -django-rainbowtests = "^0.6.0" django-debug-toolbar = "^3.2" PyJWT = "^2.0.1" ipdb = "^0.13.7" diff --git a/requirements.txt b/requirements.txt index f7f3a93c..9c5f41ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,88 +8,86 @@ autosemver==0.5.5 babel==2.9.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" backcall==0.2.0; python_version >= "3.7" black==20.8b1; python_version >= "3.6" -certifi==2021.5.30; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" -chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -click==8.0.1; python_version >= "3.6" +certifi==2021.10.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6" +charset-normalizer==2.0.9; python_full_version >= "3.6.0" and python_version >= "3.6" +click==8.0.3; python_version >= "3.6" colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0" and platform_system == "Windows" coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") datacite==1.1.2 -decorator==5.0.9; python_version >= "3.7" -django-debug-toolbar==3.2.1; python_version >= "3.6" +decorator==5.1.0; python_version >= "3.7" +django-debug-toolbar==3.2.3; python_version >= "3.6" django-environ==0.4.5 -django-rainbowtests==0.6.0 -django-split-settings==1.0.1; python_version >= "3.6" and python_version < "4.0" +django-split-settings==1.1.0; python_version >= "3.6" and python_version < "4.0" django-watchman==1.2.0 -django==3.1.13; python_version >= "3.6" -djangorestframework==3.12.4; python_version >= "3.5" +django==3.2.10; python_version >= "3.6" +djangorestframework==3.13.0; python_version >= "3.6" docutils==0.16; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" dulwich==0.19.16 -elasticsearch==7.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0" and python_version < "4") -executing==0.7.0 +elasticsearch==7.16.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0" and python_version < "4") +executing==0.8.2 gunicorn==20.1.0; python_version >= "3.5" icecream==2.1.1 -idna==2.10; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -idutils==1.1.8 -imagesize==1.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -importlib-metadata==4.6.1; python_version >= "3.6" and python_version < "3.8" +idna==3.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" +idutils==1.1.9 +imagesize==1.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +importlib-metadata==4.8.2; python_version >= "3.6" and python_version < "3.8" ipdb==0.13.9; python_version >= "2.7" -ipython-genutils==0.2.0; python_version >= "3.7" -ipython==7.25.0; python_version >= "3.7" +ipython==7.30.1; python_version >= "3.7" isbnid-fork==0.5.2 -isodate==0.6.0 -isort==5.9.1; python_full_version >= "3.6.1" and python_version < "4.0" -jedi==0.18.0; python_version >= "3.7" -jinja2==3.0.1; python_version >= "3.6" +isodate==0.6.1 +isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" +jedi==0.18.1; python_version >= "3.7" +jinja2==3.0.3; python_version >= "3.6" jsonschema==3.2.0 livereload==2.6.3; python_version >= "3.6" -lxml==4.6.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +lxml==4.7.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") markupsafe==2.0.1; python_version >= "3.6" -matplotlib-inline==0.1.2; python_version >= "3.7" +matplotlib-inline==0.1.3; python_version >= "3.7" mypy-extensions==0.4.3; python_version >= "3.6" -packaging==21.0; python_version >= "3.6" -parso==0.8.2; python_version >= "3.7" -pathspec==0.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +packaging==21.3; python_version >= "3.6" +parso==0.8.3; python_version >= "3.7" +pathspec==0.9.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.7" pickleshare==0.7.5; python_version >= "3.7" pika==1.2.0 -prompt-toolkit==3.0.19; python_full_version >= "3.6.1" and python_version >= "3.7" -psycopg2-binary==2.9.1; python_version >= "3.6" +prompt-toolkit==3.0.24; python_full_version >= "3.6.2" and python_version >= "3.7" +psycopg2-binary==2.9.2; python_version >= "3.6" ptyprocess==0.7.0; sys_platform != "win32" and python_version >= "3.7" -pygments==2.9.0; python_version >= "3.7" -pyjwt==2.1.0; python_version >= "3.6" +pygments==2.10.0; python_version >= "3.7" +pyjwt==2.3.0; python_version >= "3.6" pyoai @ git+https://github.com/infrae/pyoai@5f6eba12 -pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" +pyparsing==3.0.6; python_version >= "3.6" pyrsistent==0.18.0; python_version >= "3.6" -python-box==5.3.0; python_version >= "3.6" -python-dateutil==2.8.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") +python-box==5.4.1; python_version >= "3.6" +python-dateutil==2.8.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") python-simplexquery==1.0.5.3 -pytz==2021.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +pytz==2021.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" pyyaml==5.4.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") rdflib==5.0.0 redis==3.5.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -regex==2021.7.6; python_version >= "3.6" -requests==2.25.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -responses==0.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +regex==2021.11.10; python_version >= "3.6" +requests==2.26.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" +responses==0.13.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -snowballstemmer==2.1.0; python_version >= "3.6" +snowballstemmer==2.2.0; python_version >= "3.6" sphinx-autobuild==2021.3.14; python_version >= "3.6" sphinx-rtd-theme==0.5.2 -sphinx==4.0.3; python_version >= "3.6" +sphinx==4.3.1; python_version >= "3.6" sphinxcontrib-applehelp==1.0.2; python_version >= "3.6" sphinxcontrib-devhelp==1.0.2; python_version >= "3.6" sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.6" sphinxcontrib-jsmath==1.0.1; python_version >= "3.6" sphinxcontrib-qthelp==1.0.3; python_version >= "3.6" sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.6" -sqlparse==0.4.1; python_version >= "3.6" -structlog==21.1.0; python_version >= "3.6" +sqlparse==0.4.2; python_version >= "3.6" +structlog==21.4.0; python_version >= "3.6" tblib==1.7.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") toml==0.10.2; python_version > "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version > "3.6" and python_version < "4" tornado==6.1; python_version >= "3.6" -traitlets==5.0.5; python_version >= "3.7" -typed-ast==1.4.3; python_version >= "3.6" -typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" -urllib3==1.26.6; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" -wcwidth==0.2.5; python_full_version >= "3.6.1" and python_version >= "3.7" +traitlets==5.1.1; python_version >= "3.7" +typed-ast==1.5.1; python_version >= "3.6" +typing-extensions==4.0.1; python_version < "3.8" and python_version >= "3.6" +urllib3==1.26.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6" +wcwidth==0.2.5; python_full_version >= "3.6.2" and python_version >= "3.7" xmltodict==0.12.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") -zipp==3.5.0; python_version >= "3.6" and python_version < "3.8" +zipp==3.6.0; python_version >= "3.6" and python_version < "3.8" diff --git a/src/metax_api/api/rest/base/router.py b/src/metax_api/api/rest/base/router.py index 742103a3..ac745be9 100755 --- a/src/metax_api/api/rest/base/router.py +++ b/src/metax_api/api/rest/base/router.py @@ -27,6 +27,7 @@ DataCatalogViewSet, DatasetViewSet, DirectoryViewSet, + EditorPermissionViewSet, FileStorageViewSet, FileViewSet, SchemaViewSet, @@ -83,4 +84,9 @@ def get_default_basename(self, viewset): DatasetViewSet, ) +router.register( + "datasets/(?P.+)/editor_permissions/users", + EditorPermissionViewSet, +) + api_urlpatterns = router.urls diff --git a/src/metax_api/api/rest/base/serializers/__init__.py b/src/metax_api/api/rest/base/serializers/__init__.py index 1c243720..84d9c24b 100755 --- a/src/metax_api/api/rest/base/serializers/__init__.py +++ b/src/metax_api/api/rest/base/serializers/__init__.py @@ -13,3 +13,4 @@ from .file_storage_serializer import FileStorageSerializer from .serializer_utils import validate_json from .xml_metadata_serializer import XmlMetadataSerializer +from .editor_permissions_serializer import EditorPermissionsSerializer diff --git a/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py b/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py index bd6294ad..0b8dcfab 100755 --- a/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py +++ b/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py @@ -28,6 +28,8 @@ class Meta: "catalog_record_services_create", "catalog_record_group_read", "catalog_record_services_read", + "publish_to_ttv", + "publish_to_etsin", ) + CommonSerializer.Meta.fields extra_kwargs = CommonSerializer.Meta.extra_kwargs diff --git a/src/metax_api/api/rest/base/serializers/editor_permissions_serializer.py b/src/metax_api/api/rest/base/serializers/editor_permissions_serializer.py new file mode 100644 index 00000000..b306b416 --- /dev/null +++ b/src/metax_api/api/rest/base/serializers/editor_permissions_serializer.py @@ -0,0 +1,20 @@ +from django.core.validators import EMPTY_VALUES + +from rest_framework.serializers import ValidationError, ModelSerializer + +from metax_api.models import EditorUserPermission + +from .common_serializer import CommonSerializer + + +class EditorPermissionsSerializer(ModelSerializer): + class Meta: + model = EditorUserPermission + fields = "__all__" + + extra_kwargs = CommonSerializer.Meta.extra_kwargs + + def validate(self, attrs): + data = ModelSerializer.validate(self, attrs) + + return data diff --git a/src/metax_api/api/rest/base/views/__init__.py b/src/metax_api/api/rest/base/views/__init__.py index d5c62dd9..027ae76f 100755 --- a/src/metax_api/api/rest/base/views/__init__.py +++ b/src/metax_api/api/rest/base/views/__init__.py @@ -10,6 +10,7 @@ from .data_catalog_view import DataCatalogViewSet from .dataset_view import DatasetViewSet from .directory_view import DirectoryViewSet +from .editor_permissions_view import EditorPermissionViewSet from .file_storage_view import FileStorageViewSet from .file_view import FileViewSet from .schema_view import SchemaViewSet diff --git a/src/metax_api/api/rest/base/views/common_view.py b/src/metax_api/api/rest/base/views/common_view.py index f5f0be75..053a5a40 100755 --- a/src/metax_api/api/rest/base/views/common_view.py +++ b/src/metax_api/api/rest/base/views/common_view.py @@ -176,11 +176,11 @@ def get_queryset(self): q_filters = [] deduplicated_q_filters = [] - CS.set_if_modified_since_filter(self.request, additional_filters) - if hasattr(self, "queryset_search_params"): additional_filters.update(**self.queryset_search_params) + CS.set_if_modified_since_filter(self.request, additional_filters) + if "q_filters" in additional_filters: # Q-filter objects, which can contain more complex filter options such as OR-clauses q_filters = additional_filters.pop("q_filters") @@ -377,7 +377,9 @@ def _check_and_store_bulk_error(self, request, response): """ if "failed" in response.data and len(response.data["failed"]): try: - error_json = ApiErrorSerializerV2.request_to_json(self.request, response, other={"bulk_request": True}) + error_json = ApiErrorSerializerV2.request_to_json( + self.request, response, other={"bulk_request": True} + ) response.data["error_identifier"] = error_json["identifier"] if settings.ENABLE_API_ERROR_OBJECTS: rabbitmq.publish(error_json, exchange="apierrors") diff --git a/src/metax_api/api/rest/base/views/dataset_view.py b/src/metax_api/api/rest/base/views/dataset_view.py index bd416afa..648feb45 100755 --- a/src/metax_api/api/rest/base/views/dataset_view.py +++ b/src/metax_api/api/rest/base/views/dataset_view.py @@ -10,6 +10,7 @@ from django.conf import settings from django.http import Http404 + from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response @@ -270,7 +271,7 @@ def _search_using_dataset_identifiers(self): @action(detail=True, methods=["get"], url_path="redis") def redis_test(self, request, pk=None): # pragma: no cover if request.user.username != "metax": - raise Http403() + raise Http403({"detail": ["Access denied."]}) try: cached = self.cache.get("cr-1211%s" % pk) except: @@ -295,7 +296,7 @@ def redis_test(self, request, pk=None): # pragma: no cover @action(detail=True, methods=["get"], url_path="rabbitmq") def rabbitmq_test(self, request, pk=None): # pragma: no cover if request.user.username != "metax": - raise Http403() + raise Http403({"detail": ["Access denied."]}) rabbitmq.publish({"msg": "hello create"}, routing_key="create", exchange="datasets") rabbitmq.publish({"msg": "hello update"}, routing_key="update", exchange="datasets") return Response(data={}, status=status.HTTP_200_OK) @@ -311,7 +312,7 @@ def update_cr_total_files_byte_sizes(self, request): :return: """ if request.user.username != "metax": - raise Http403() + raise Http403({"detail": ["Access denied."]}) # Get all IDs for ida data catalogs ida_catalog_ids = [] for dc in DataCatalog.objects.filter( @@ -340,7 +341,7 @@ def update_cr_directory_browsing_data(self, request): :return: """ if request.user.username != "metax": - raise Http403() + raise Http403({"detail": ["Access denied."]}) if "id" in request.query_params: # in order to update one record only, use query param ?id=integer. useful for testcases @@ -389,7 +390,7 @@ def flush_password(self, request): # pragma: no cover with open("/home/metax-user/flush_password", "w") as f: dump(request.data, f) else: - raise Http403 + raise Http403({"detail": ["Access denied."]}) _logger.debug("FLUSH password set") return Response(data=None, status=status.HTTP_204_NO_CONTENT) @@ -421,4 +422,4 @@ def flush_records(self, request): # pragma: no cover _logger.debug("FLUSH called by %s" % request.user.username) return Response(data=None, status=status.HTTP_204_NO_CONTENT) - return Response(data=None, status=status.HTTP_403_FORBIDDEN) + return Response({"error": "Access denied"}, status=status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/api/rest/base/views/directory_view.py b/src/metax_api/api/rest/base/views/directory_view.py index 587363c2..1f45461e 100755 --- a/src/metax_api/api/rest/base/views/directory_view.py +++ b/src/metax_api/api/rest/base/views/directory_view.py @@ -163,7 +163,7 @@ def update_byte_sizes_and_file_counts(self, request): # pragma: no cover correct mistakes in real data. """ if request.user.username != "metax": - raise Http403 + raise Http403({"detail": ["Access denied."]}) for p in ( Directory.objects.all() diff --git a/src/metax_api/api/rest/base/views/editor_permissions_view.py b/src/metax_api/api/rest/base/views/editor_permissions_view.py new file mode 100644 index 00000000..2c87c001 --- /dev/null +++ b/src/metax_api/api/rest/base/views/editor_permissions_view.py @@ -0,0 +1,144 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT +import datetime +import logging + +from django.core.validators import EMPTY_VALUES + +from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework.response import Response + +from metax_api.exceptions import Http403 +from metax_api.models import CatalogRecord +from metax_api.models.catalog_record import PermissionRole, EditorUserPermission +from metax_api.permissions import ServicePermissions, EndUserPermissions +from metax_api.services import CommonService + +from ..serializers import EditorPermissionsSerializer +from .common_view import CommonViewSet + +_logger = logging.getLogger(__name__) + + +class EditorPermissionViewSet(CommonViewSet): + lookup_field = "user_id" + permission_classes = [ + ServicePermissions, + EndUserPermissions, + ] + serializer_class = EditorPermissionsSerializer + + def __init__(self, *args, **kwargs): + super(EditorPermissionViewSet, self).__init__(*args, **kwargs) + + def _user_has_access_to_permissions(self, cr): + """ + Allow only services and dataset owner access permissions. + """ + if self.request.user.is_service: + if self.request.method == "GET": + return True + # for updating perms, require edit access to catalog records + return cr._check_catalog_permissions( + cr.data_catalog.catalog_record_group_edit, + cr.data_catalog.catalog_record_services_edit, + self.request, + ) + if cr.user_is_owner(self.request): + return True + # unknown user + return False + + def get_queryset(self): + if CommonService.is_primary_key(self.kwargs["cr_identifier"]): + cr = get_object_or_404(CatalogRecord, pk=int(self.kwargs["cr_identifier"])) + else: + cr = get_object_or_404(CatalogRecord, identifier=self.kwargs["cr_identifier"]) + if not self._user_has_access_to_permissions(cr): + raise Http403({"detail": ["You do not have access to permissions of this dataset."]}) + return cr.editor_permissions.users + + def list(self, request, *args, **kwargs): + users = self.get_queryset() + editorserializer = EditorPermissionsSerializer(users.all(), many=True) + return Response(editorserializer.data) + + def create(self, request, *args, **kwargs): + data = request.data + + perms_id = self.get_queryset().instance.id + if "user_id" not in data: + return Response({"user_id": "Missing user_id"}, status=status.HTTP_400_BAD_REQUEST) + + editorserializer = None + try: + removed_user = EditorUserPermission.objects_unfiltered.get( + user_id=data.get("user_id"), editor_permissions_id=perms_id + ) + except EditorUserPermission.DoesNotExist: + removed_user = None + + if removed_user not in EMPTY_VALUES and removed_user.removed is True: + data['date_modified'] = datetime.datetime.now() + data['date_removed'] = None + data['removed'] = False + editorserializer = EditorPermissionsSerializer(removed_user, data=data, partial=True) + elif removed_user not in EMPTY_VALUES and removed_user.removed is False: + return Response( + {'user_id': "User_id already exists"}, status=status.HTTP_400_BAD_REQUEST + ) + else: + data['editor_permissions'] = perms_id + data['date_created'] = datetime.datetime.now() + editorserializer = EditorPermissionsSerializer(data=data) + if editorserializer.is_valid(): + editorserializer.save() + return Response(editorserializer.data, status=status.HTTP_201_CREATED) + else: + return Response(editorserializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, *args, **kwargs): + user = self.get_object() + users = self.get_queryset() + + creators = users.filter(role=PermissionRole.CREATOR, removed=False).count() + if user.role == PermissionRole.CREATOR and creators < 2: + return Response( + {"error": "Can't delete last creator"}, status=status.HTTP_400_BAD_REQUEST + ) + else: + user.remove() + return Response(status=status.HTTP_200_OK) + + def partial_update(self, request, **kwargs): + data = request.data + user = self.get_object() + users = self.get_queryset() + + if 'role' in data and user.role == PermissionRole.CREATOR: + creators = users.filter(role=PermissionRole.CREATOR, removed=False).count() + if creators < 2 and data.get('role') != PermissionRole.CREATOR: + return Response({"error": "Can't change last creator"}, status=status.HTTP_400_BAD_REQUEST) + + data['date_modified'] = datetime.datetime.now() + serializer = EditorPermissionsSerializer(user, data=data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def update_bulk(self, request, *args, **kwargs): + return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) + + def partial_update_bulk(self, request, *args, **kwargs): + return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) + + def destroy_bulk(self, request, *args, **kwargs): + return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) + diff --git a/src/metax_api/api/rest/v2/router.py b/src/metax_api/api/rest/v2/router.py index 46e703d5..10bcd0c9 100755 --- a/src/metax_api/api/rest/v2/router.py +++ b/src/metax_api/api/rest/v2/router.py @@ -29,6 +29,7 @@ FileStorageViewSet, FileViewSet, SchemaViewSet, + EditorPermissionViewSet, ) from metax_api.api.rest.v2.views import ApiErrorViewSetV2 @@ -110,4 +111,9 @@ def __init__(self, *args, **kwargs): DatasetViewSet, ) +router_v2.register( + "datasets/(?P.+)/editor_permissions/users", + EditorPermissionViewSet, +) + api_urlpatterns = router_v1.urls + router_v2.urls diff --git a/src/metax_api/api/rest/v2/views/api_error_view.py b/src/metax_api/api/rest/v2/views/api_error_view.py index c0e477d4..ac926d70 100644 --- a/src/metax_api/api/rest/v2/views/api_error_view.py +++ b/src/metax_api/api/rest/v2/views/api_error_view.py @@ -29,7 +29,7 @@ class ApiErrorViewSetV2(CommonViewSet): def initial(self, request, *args, **kwargs): if request.user.username != "metax": - raise Http403 + raise Http403({"detail": ["Access denied."]}) return super().initial(request, *args, **kwargs) @action(detail=False, methods=["post"], url_path="flush") diff --git a/src/metax_api/api/rest/v2/views/dataset_view.py b/src/metax_api/api/rest/v2/views/dataset_view.py index 84eb1776..29e361e1 100755 --- a/src/metax_api/api/rest/v2/views/dataset_view.py +++ b/src/metax_api/api/rest/v2/views/dataset_view.py @@ -41,7 +41,7 @@ def projects_list(self, request, pk=None): cr = self.get_object() if not cr.user_is_privileged(request): - raise Http403 + raise Http403({"detail": ["Access denied."]}) projects = [ p diff --git a/src/metax_api/api/rpc/base/views/dataset_rpc.py b/src/metax_api/api/rpc/base/views/dataset_rpc.py index 9a8f2336..df35d13e 100755 --- a/src/metax_api/api/rpc/base/views/dataset_rpc.py +++ b/src/metax_api/api/rpc/base/views/dataset_rpc.py @@ -9,6 +9,7 @@ from json import load from django.conf import settings as django_settings +from django.db import connection from django.http import Http404 from rest_framework import status from rest_framework.decorators import action @@ -198,3 +199,44 @@ def _save_and_publish_dataset(self, cr, action): super(CatalogRecord, cr).save(update_fields=["preservation_identifier"]) DataciteDOIUpdate(cr, cr.preservation_identifier, action)() + + @action(detail=False, methods=["post"], url_path="flush_user_data") + def flush_user_data(self, request): + """ + Permanently delete datasets created by a certain user. + + WARNING! Not meant for active production use!! + """ + if django_settings.ENV == "production": + raise Http400({"detail": ["API currently allowed only in test environments"]}) + + if "metadata_provider_user" not in request.query_params: + raise Http400({"detail": ["metadata_provider_user is a required query parameter"]}) + + user = request.query_params["metadata_provider_user"] + + sql_delete_cr_files = """ + delete from metax_api_catalogrecord_files + where catalogrecord_id in ( + select id from metax_api_catalogrecord where metadata_provider_user = %s + ) + """ + + sql_delete_datasets = """ + delete from metax_api_catalogrecord where metadata_provider_user = %s + """ + + _logger.info( + "Flushing datasets for user %s on the request of user: %s" % (user, request.user.username) + ) + + with connection.cursor() as cr: + cr.execute(sql_delete_cr_files, [user]) + cr.execute(sql_delete_datasets, [user]) + if cr.rowcount == 0: + _logger.info("No datasets found for user %s" % user) + return Response(user, status=status.HTTP_404_NOT_FOUND) + + _logger.info("Permanently deleted all datasets created by user %s" % user) + + return Response(data=None, status=status.HTTP_204_NO_CONTENT) diff --git a/src/metax_api/api/rpc/base/views/file_rpc.py b/src/metax_api/api/rpc/base/views/file_rpc.py index eb8dd169..e4567b98 100755 --- a/src/metax_api/api/rpc/base/views/file_rpc.py +++ b/src/metax_api/api/rpc/base/views/file_rpc.py @@ -33,7 +33,7 @@ def delete_project(self, request): return FileService.delete_project(request.query_params["project_identifier"]) @action(detail=False, methods=["post"], url_path="flush_project") - def flush_project(self, request): # pragma: no cover + def flush_project(self, request): """ Permanently delete an entire project's files and directories. @@ -70,6 +70,9 @@ def flush_project(self, request): # pragma: no cover cr.execute(sql_delete_cr_files, [project]) cr.execute(sql_delete_files, [project]) cr.execute(sql_delete_directories, [project]) + if cr.rowcount == 0: + _logger.info("No files or directories found for project %s" % project) + return Response(project, status=status.HTTP_404_NOT_FOUND) _logger.info("Permanently deleted all files and directories from project %s" % project) diff --git a/src/metax_api/api/rpc/base/views/statistic_rpc.py b/src/metax_api/api/rpc/base/views/statistic_rpc.py index 96c4713f..aa7479d1 100755 --- a/src/metax_api/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/api/rpc/base/views/statistic_rpc.py @@ -131,6 +131,11 @@ def organization_datasets_cumulative(self, request): "to_date": request.query_params.get("to_date", None), "metadata_owner_org": request.query_params.get("metadata_owner_org", None), } + + for boolean_param in ["latest", "legacy", "removed"]: + if boolean_param in request.query_params: + params[boolean_param] = CS.get_boolean_query_param(request, boolean_param) + return Response(StatisticService.total_organization_datasets(**params)) @action(detail=False, methods=["get"], url_path="unused_files") diff --git a/src/metax_api/initialdata/datacatalogs.json b/src/metax_api/initialdata/datacatalogs.json index 539a26d1..4a604446 100755 --- a/src/metax_api/initialdata/datacatalogs.json +++ b/src/metax_api/initialdata/datacatalogs.json @@ -71,9 +71,11 @@ "research_dataset_schema": "ida", "logo": "fairdata_tree_logo.svg" }, - "catalog_record_services_edit": "ida,metax,qvain,qvain-light,tpas", - "catalog_record_services_create": "ida,metax,qvain,qvain-light,tpas", - "catalog_record_services_read": "ida,metax,qvain,qvain-light,etsin,tpas,download" + "catalog_record_services_edit": "ida,metax,qvain,qvain-light,tpas", + "catalog_record_services_create": "ida,metax,qvain,qvain-light,tpas", + "catalog_record_services_read": "ida,metax,qvain,qvain-light,etsin,tpas,download", + "publish_to_etsin": true, + "publish_to_ttv": true }, { "catalog_json": { @@ -148,9 +150,11 @@ "research_dataset_schema": "att", "logo": "fairdata_tree_logo.svg" }, - "catalog_record_services_edit": "ida,metax,qvain,qvain-light,tpas", - "catalog_record_services_create": "ida,metax,qvain,qvain-light,tpas", - "catalog_record_services_read": "ida,metax,qvain,qvain-light,etsin,tpas,download" + "catalog_record_services_edit": "ida,metax,qvain,qvain-light,tpas", + "catalog_record_services_create": "ida,metax,qvain,qvain-light,tpas", + "catalog_record_services_read": "ida,metax,qvain,qvain-light,etsin,tpas,download", + "publish_to_etsin": true, + "publish_to_ttv": true }, { "catalog_json": { @@ -226,11 +230,13 @@ "research_dataset_schema": "ida", "logo": "fairdata_tree_logo.svg" }, - "catalog_record_group_edit": "pas_edit", - "catalog_record_group_create": "pas_create", - "catalog_record_services_edit": "metax,tpas", - "catalog_record_services_create": "metax,tpas", - "catalog_record_services_read": "metax,tpas,download" + "catalog_record_group_edit": "pas_edit", + "catalog_record_group_create": "pas_create", + "catalog_record_services_edit": "metax,tpas", + "catalog_record_services_create": "metax,tpas", + "catalog_record_services_read": "metax,tpas,download", + "publish_to_etsin": true, + "publish_to_ttv": true }, { "catalog_json": { @@ -298,7 +304,9 @@ }, "catalog_record_services_edit": "metax,qvain,qvain-light,tpas", "catalog_record_services_create": "metax,qvain,qvain-light,tpas", - "catalog_record_services_read": "metax,qvain,qvain-light,tpas,download" + "catalog_record_services_read": "metax,qvain,qvain-light,tpas,download", + "publish_to_etsin": false, + "publish_to_ttv": false }, { "catalog_json": { @@ -354,9 +362,11 @@ "dataset_versioning": false, "research_dataset_schema": "dft" }, - "catalog_record_services_edit": "metax,qvain,qvain-light,tpas, ida, testuser, api_auth_user", - "catalog_record_services_create": "metax,qvain,qvain-light,tpas, ida, testuser, api_auth_user", - "catalog_record_services_read": "metax,qvain,qvain-light,etsin,tpas, ida, testuser, api_auth_user,download" + "catalog_record_services_edit": "metax,qvain,qvain-light,tpas, ida, testuser, api_auth_user", + "catalog_record_services_create": "metax,qvain,qvain-light,tpas, ida, testuser, api_auth_user", + "catalog_record_services_read": "metax,qvain,qvain-light,etsin,tpas, ida, testuser, api_auth_user,download", + "publish_to_etsin": true, + "publish_to_ttv": true }, { "catalog_json": { @@ -417,5 +427,251 @@ }, "catalog_record_services_edit": "metax,jyu", "catalog_record_services_create": "metax,jyu", - "catalog_record_services_read": "metax,jyu,download" + "catalog_record_services_read": "metax,jyu,download", + "publish_to_etsin": true, + "publish_to_ttv": true +}, +{ + "catalog_json": { + "title": { + "en": "Reportronic catalog", + "fi": "Reportronic katalogi" + }, + "language": [ + { + "identifier": "http://lexvo.org/id/iso639-3/fin" + } + ], + "harvested": false, + "publisher": { + "name": { + "en": "Reportronic", + "fi": "Reportronic" + } + }, + "identifier": "urn:nbn:fi:att:data-catalog-reportronic", + "access_rights": { + "license": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0" + } + ], + "access_type": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } + } + ], + "description": { + "en": "Contains datasets from Reportronic service", + "fi": "Sisältää aineistoja Reportronic-palvelusta" + } + }, + "dataset_versioning": false, + "research_dataset_schema": "att" + }, + "catalog_record_services_edit": "metax,reportronic", + "catalog_record_services_create": "metax,reportronic", + "catalog_record_services_read": "metax,reportronic", + "publish_to_etsin": false, + "publish_to_ttv": true +}, +{ + "catalog_json": { + "title": { + "en": "ACRIS catalog", + "fi": "ACRIS katalogi" + }, + "language": [ + { + "identifier": "http://lexvo.org/id/iso639-3/fin" + } + ], + "harvested": true, + "publisher": { + "name": { + "en": "Aalto ACRIS", + "fi": "Aalto ACRIS" + }, + "homepage": [{ + "title": { + "en": "https://research.aalto.fi", + "fi": "https://research.aalto.fi" + }, + "identifier": "https://research.aalto.fi" + }] + }, + "identifier": "urn:nbn:fi:att:data-catalog-acris", + "access_rights": { + "license": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0" + } + ], + "access_type": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } + } + ], + "description": { + "en": "Contains datasets from Aalto University's ACRIS system", + "fi": "Sisältää aineistoja Aalto yliopiston ACRIS-järjestelmästä" + } + }, + "dataset_versioning": false, + "research_dataset_schema": "att" + }, + "catalog_record_services_edit": "metax,aalto", + "catalog_record_services_create": "metax,aalto", + "catalog_record_services_read": "metax,aalto", + "publish_to_etsin": true, + "publish_to_ttv": true +}, +{ + "catalog_json": { + "title": { + "en": "Finnish Meteorological Institute EUDAT B2SHARE catalog", + "fi": "Ilmatieteen laitoksen EUDAT B2SHARE -katalogi", + "sv": "Meteorologiska Institutet EUDAT B2SHARE register" + }, + "language": [ + { + "title": { + "en": "Finnish language", + "fi": "Suomen kieli", + "sv": "finska", + "und": "Suomen kieli" + }, + "identifier": "http://lexvo.org/id/iso639-3/fin" + }, + { + "title": { + "en": "English language", + "fi": "Englannin kieli", + "sv": "engelska", + "und": "Englannin kieli" + }, + "identifier": "http://lexvo.org/id/iso639-3/eng" + } + ], + "harvested": true, + "publisher": { + "name": { + "en": "Finnish Meteorological Institute", + "fi": "Ilmatieteen laitos", + "sv": "Meteorologiska Institutet" + }, + "homepage": [ + { + "title": { + "en": "Home - Finnish Meteorological Institute", + "fi": "Etusivu - Ilmatieteen laitos", + "sv": "Startsida - Meteorologiska institutet" + }, + "identifier": "https://www.ilmatieteenlaitos.fi/" + } + ], + "identifier": "https://isni.org/isni/0000000122538678" + }, + "identifier": "urn:nbn:fi:att:data-catalog-fmi", + "access_rights": { + "license": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0" + } + ], + "access_type": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } + } + ], + "description": { + "en": "Contains datasets of Finnish Meteorological Institute harvested from FMI's EUDAT B2SHARE instance", + "fi": "Sisältää aineistoja, jotka on kerätty Ilmatieteen laitoksen EUDAT B2SHARE -instanssista" + } + }, + "dataset_versioning": false, + "research_dataset_schema": "att" + }, + "catalog_record_services_edit": "metax,eudat", + "catalog_record_services_create": "metax,eudat", + "catalog_record_services_read": "metax,eudat", + "publish_to_etsin": true, + "publish_to_ttv": true +}, +{ + "catalog_json": { + "title": { + "en": "CSC SD services datasets", + "fi": "CSC:n Arkaluonteisen datan palveluiden aineistokatalogi" + }, + "language": [ + { + "title": { + "en": "English language", + "fi": "Englannin kieli", + "sv": "engelska", + "und": "Englannin kieli" + }, + "identifier": "http://lexvo.org/id/iso639-3/eng" + } + ], + "harvested": true, + "publisher": { + "name": { + "en": "CSC Sensitive Data Services for Research" + }, + "homepage": [ + { + "title": { + "en": "Sensitive Data Services for Research - Services for Research - CSC Company Site" + }, + "identifier": "https://research.csc.fi/sensitive-data-services-for-research" + } + ] + }, + "identifier": "urn:nbn:fi:att:data-catalog-sd", + "access_rights": { + "license": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0" + } + ], + "access_type": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } + } + ], + "description": { + "en": "Datasets stored in CSC SD services", + "fi": "Sisältää aineistoja CSC:n SD-palvelusta" + } + }, + "dataset_versioning": false, + "research_dataset_schema": "att" + }, + "catalog_record_services_edit": "metax,sd", + "catalog_record_services_create": "metax,sd", + "catalog_record_services_read": "metax,sd", + "publish_to_etsin": true, + "publish_to_ttv": true }] \ No newline at end of file diff --git a/src/metax_api/management/commands/mark_files_removed.py b/src/metax_api/management/commands/mark_files_removed.py index 22c9ec86..b00bcfcc 100644 --- a/src/metax_api/management/commands/mark_files_removed.py +++ b/src/metax_api/management/commands/mark_files_removed.py @@ -32,6 +32,9 @@ def handle(self, *args, **options): removed_files_sum = 0 for prefix in path_prefixes: + if not prefix.strip(): + logger.info("Prefix is empty. Skipping.") + continue files = File.objects.filter(project_identifier = options["project_identifier"], file_path__startswith = prefix, removed = "f") logger.info(f"Found {len(files)} files to remove in project: {options['project_identifier']} with path prefix: {prefix}") for file in files: diff --git a/src/metax_api/middleware/identifyapicaller.py b/src/metax_api/middleware/identifyapicaller.py index 85b04e36..635de74a 100755 --- a/src/metax_api/middleware/identifyapicaller.py +++ b/src/metax_api/middleware/identifyapicaller.py @@ -108,7 +108,7 @@ def _identify_api_caller(self, request): "Unauthenticated access attempt from ip: %s. Authorization header missing" % request.META["REMOTE_ADDR"] ) - raise Http403 + raise Http403({"detail": ["Access denied."]}) try: auth_method, auth_b64 = http_auth_header.split(" ") @@ -156,10 +156,10 @@ def _auth_basic(self, request, auth_b64): if not user: _logger.warning("Failed authnz for user %s: user not found" % username) - raise Http403 + raise Http403({"detail": ["Access denied."]}) if apikey != user["password"]: _logger.warning("Failed authnz for user %s: password mismatch" % username) - raise Http403 + raise Http403({"detail": ["Access denied."]}) request.user.username = username request.user.is_service = True @@ -178,7 +178,7 @@ def _auth_bearer(self, request, auth_b64): if response.status_code != 200: _logger.warning("Bearer token validation failed") - raise Http403 + raise Http403({"detail": ["Access denied."]}) try: token = self._extract_id_token(auth_b64) @@ -186,13 +186,13 @@ def _auth_bearer(self, request, auth_b64): # the above method should never fail, as this code should not be # reachable if the token validation had already failed. _logger.exception("Failed to extract token from id_token string") - raise Http403 + raise Http403({"detail": ["Access denied."]}) if len(token.get("CSCUserName", "")) > 0: request.user.username = token["CSCUserName"] else: _logger.warning("id_token does not contain valid user id: fairdataid or cscusername") - raise Http403 + raise Http403({"detail": ["Access denied."]}) request.user.is_service = False request.user.token = token diff --git a/src/metax_api/middleware/stream_http_response.py b/src/metax_api/middleware/stream_http_response.py index 5ca379a9..5731743c 100755 --- a/src/metax_api/middleware/stream_http_response.py +++ b/src/metax_api/middleware/stream_http_response.py @@ -38,8 +38,9 @@ def __call__(self, request): and self._check_query_param(request, "stream") ): resp = StreamingHttpResponse(self._stream_response(response)) - resp._headers["content-type"] = ("Content-Type", "application/json") - resp._headers["x-count"] = ("X-Count", str(len(response.data))) + resp.headers["content-type"] = ("Content-Type", "application/json") + + resp.headers["x-count"] = ("X-Count", str(len(response.data))) return resp return response diff --git a/src/metax_api/migrations/0041_add_editorpermissions.py b/src/metax_api/migrations/0041_add_editorpermissions.py new file mode 100644 index 00000000..96a38c29 --- /dev/null +++ b/src/metax_api/migrations/0041_add_editorpermissions.py @@ -0,0 +1,195 @@ +# Generated by Django 3.1.13 on 2021-08-26 11:14 + +from django.db import migrations, models +import django.db.models.deletion +from django.utils import timezone +import logging +import uuid +from metax_api.models import catalog_record + +logger = logging.getLogger(__name__) + + +def add_permissions(apps, schema_editor): + """ + Add EditorPermissions for each version set and related next_draft CatalogRecords. + + Here CatalogRecords not belonging to a DatasetVersionSet are considered a version set with size 1. + """ + CatalogRecord = apps.get_model("metax_api", "CatalogRecordV2") + EditorUserPermission = apps.get_model("metax_api", "EditorUserPermission") + EditorPermissions = apps.get_model("metax_api", "EditorPermissions") + + num_perms = 0 + num_datasets = 0 + prev_version_set_id = None + version_set_users = [] + version_set_crs = [] + + def flush_version_set(): + """Create single EditorPermissions object for each version set, add creator user""" + nonlocal num_perms, num_datasets, version_set_crs, version_set_users + if len(version_set_crs) > 0: + permissions = EditorPermissions.objects.create() + permissions.catalog_records.add(*version_set_crs) + num_perms += 1 + num_datasets += len(version_set_crs) + + for user in version_set_users: + now = timezone.now().replace(microsecond=0) + EditorUserPermission.objects.create( + editor_permissions=permissions, + user_id=user, + verified=True, + role="creator", + date_created=now, + ) + version_set_users = [] + version_set_crs = [] + + # group datasets by version_sets and include their next_draft datasets + for cr in CatalogRecord.objects.filter(draft_of__isnull=True).order_by( + "dataset_version_set_id", "id" + ): + if cr.dataset_version_set_id is None or cr.dataset_version_set_id != prev_version_set_id: + flush_version_set() + + version_set_crs.append(cr) + if cr.next_draft: + version_set_crs.append(cr.next_draft) + + # DatasetVersionSet shouldn't have multiple metadata_provider_users but this supports them just in case + if cr.metadata_provider_user and cr.metadata_provider_user not in version_set_users: + version_set_users.append(cr.metadata_provider_user) + + prev_version_set_id = cr.dataset_version_set_id + flush_version_set() + + logger.info(f"Added {num_perms} EditorPermissions to {num_datasets} datasets") + + +def revert(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("metax_api", "0040_auto_20211006_1116"), + ] + + operations = [ + migrations.CreateModel( + name="EditorPermissions", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ], + ), + migrations.CreateModel( + name="EditorUserPermission", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("active", models.BooleanField(default=True)), + ("removed", models.BooleanField(default=False)), + ("date_modified", models.DateTimeField(null=True)), + ("user_modified", models.CharField(max_length=200, null=True)), + ("date_created", models.DateTimeField()), + ("user_created", models.CharField(max_length=200, null=True)), + ( + "service_modified", + models.CharField( + help_text="Name of the service who last modified the record", + max_length=200, + null=True, + ), + ), + ( + "service_created", + models.CharField( + help_text="Name of the service who created the record", + max_length=200, + null=True, + ), + ), + ("date_removed", models.DateTimeField(null=True)), + ("user_id", models.CharField(max_length=200)), + ( + "role", + models.CharField( + choices=[("creator", "Creator"), ("editor", "Editor")], + max_length=16, + ), + ), + ("verified", models.BooleanField(default=False)), + ("verification_token", models.CharField(max_length=32, null=True)), + ("verification_token_expires", models.DateTimeField(null=True)), + ( + "editor_permissions", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="users", + to="metax_api.editorpermissions", + ), + ), + ], + ), + migrations.AddField( + model_name="catalogrecord", + name="editor_permissions", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="catalog_records", + to="metax_api.editorpermissions", + ), + ), + migrations.AddIndex( + model_name="editoruserpermission", + index=models.Index(fields=["user_id"], name="metax_api_e_user_id_0b47cc_idx"), + ), + migrations.AddConstraint( + model_name="editoruserpermission", + constraint=models.UniqueConstraint( + fields=("editor_permissions", "user_id"), + name="unique_dataset_user_permission", + ), + ), + migrations.AddConstraint( + model_name="editoruserpermission", + constraint=models.CheckConstraint( + check=models.Q(_negated=True, user_id=""), name="require_user_id" + ), + ), + migrations.AddConstraint( + model_name="editoruserpermission", + constraint=models.CheckConstraint( + check=models.Q(role__in=["creator", "editor"]), name="require_role" + ), + ), + migrations.RunPython(add_permissions, revert), + migrations.AlterField( + model_name="catalogrecord", + name="editor_permissions", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="catalog_records", + to="metax_api.editorpermissions", + ), + ), + ] diff --git a/src/metax_api/migrations/0042_auto_20211108_1256.py b/src/metax_api/migrations/0042_auto_20211108_1256.py new file mode 100644 index 00000000..51e1afcb --- /dev/null +++ b/src/metax_api/migrations/0042_auto_20211108_1256.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1.13 on 2021-11-08 10:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0041_add_editorpermissions'), + ] + + operations = [ + migrations.RemoveField( + model_name='editoruserpermission', + name='verification_token', + ), + migrations.RemoveField( + model_name='editoruserpermission', + name='verification_token_expires', + ), + ] diff --git a/src/metax_api/migrations/0043_remove_editoruserpermission_verified.py b/src/metax_api/migrations/0043_remove_editoruserpermission_verified.py new file mode 100644 index 00000000..220acfab --- /dev/null +++ b/src/metax_api/migrations/0043_remove_editoruserpermission_verified.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.13 on 2021-11-10 09:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0042_auto_20211108_1256'), + ] + + operations = [ + migrations.RemoveField( + model_name='editoruserpermission', + name='verified', + ), + ] diff --git a/src/metax_api/migrations/0044_change_aalto_catalog_to_harvested.py b/src/metax_api/migrations/0044_change_aalto_catalog_to_harvested.py new file mode 100755 index 00000000..54ca0604 --- /dev/null +++ b/src/metax_api/migrations/0044_change_aalto_catalog_to_harvested.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.13 on 2021-12-16 13:39 + +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + +import logging + +logger = logging.getLogger(__name__) + +def change_aalto_catalog_harvested_value(apps, schema_editor, harvested_value = True): + logger.info(f"Change Aalto catalog harvested value to: {harvested_value}") + try: + DataCatalog = apps.get_model('metax_api', 'DataCatalog') + aalto_catalog = DataCatalog.objects.get(catalog_json__identifier = "urn:nbn:fi:att:data-catalog-acris") + aalto_catalog.catalog_json["harvested"] = harvested_value + aalto_catalog.save() + except DataCatalog.DoesNotExist: + logger.info("Aalto catalog does not exist. Passing") + pass + +def revert(apps, schema_editor): + change_aalto_catalog_harvested_value(apps, schema_editor, harvested_value = False) + + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0043_remove_editoruserpermission_verified'), + ] + + operations = [ + migrations.RunPython(change_aalto_catalog_harvested_value, revert), + ] diff --git a/src/metax_api/migrations/0045_add_publish_fields_to_catalogs.py b/src/metax_api/migrations/0045_add_publish_fields_to_catalogs.py new file mode 100644 index 00000000..6c09ef02 --- /dev/null +++ b/src/metax_api/migrations/0045_add_publish_fields_to_catalogs.py @@ -0,0 +1,50 @@ +from django.db import migrations, models + +import logging + +logger = logging.getLogger(__name__) + + +def add_publish_fields_to_catalogs(apps, schema_editor): + logger.info("Add publish_to_etsin and publish_to_ttv fields to data catalogs") + + dont_publish_to_etsin = ["urn:nbn:fi:att:data-catalog-legacy", "urn:nbn:fi:att:data-catalog-reportronic", "urn:nbn:fi:att:data-catalog-repotronic"] + dont_publish_to_ttv = ["urn:nbn:fi:att:data-catalog-legacy"] + + DataCatalog = apps.get_model('metax_api', 'DataCatalog') + catalogs = DataCatalog.objects.all() + + for catalog in catalogs: + if catalog.catalog_json["identifier"] in dont_publish_to_etsin: + catalog.publish_to_etsin = False + + if catalog.catalog_json["identifier"] in dont_publish_to_ttv: + catalog.publish_to_ttv = False + + catalog.save() + + +def revert(apps, schema_editor): + pass + + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0044_change_aalto_catalog_to_harvested'), + ] + + operations = [ + migrations.AddField( + model_name='datacatalog', + name='publish_to_etsin', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='datacatalog', + name='publish_to_ttv', + field=models.BooleanField(default=True), + ), + migrations.RunPython(add_publish_fields_to_catalogs, revert), + ] diff --git a/src/metax_api/models/__init__.py b/src/metax_api/models/__init__.py index ec08fb44..643cac39 100755 --- a/src/metax_api/models/__init__.py +++ b/src/metax_api/models/__init__.py @@ -6,7 +6,7 @@ # :license: MIT from .api_error import ApiError -from .catalog_record import AlternateRecordSet, CatalogRecord +from .catalog_record import AlternateRecordSet, CatalogRecord, EditorPermissions, EditorUserPermission from .catalog_record_v2 import CatalogRecordV2 from .common import Common from .contract import Contract diff --git a/src/metax_api/models/catalog_record.py b/src/metax_api/models/catalog_record.py index 9a499bde..0c7dd9df 100755 --- a/src/metax_api/models/catalog_record.py +++ b/src/metax_api/models/catalog_record.py @@ -6,17 +6,21 @@ # :license: MIT import logging +import uuid from collections import defaultdict from copy import deepcopy +from datetime import datetime, timedelta from django.conf import settings from django.contrib.postgres.fields import ArrayField from django.db import connection, models, transaction from django.db.models import JSONField, Q, Sum from django.http import Http404 +from django.utils.crypto import get_random_string from rest_framework.serializers import ValidationError from metax_api.exceptions import Http400, Http403, Http503 +from metax_api.tasks.refdata.refdata_indexer import service from metax_api.utils import ( DelayedLog, IdentifierType, @@ -58,6 +62,61 @@ DFT_CATALOG = settings.DFT_DATA_CATALOG_IDENTIFIER +class EditorPermissions(models.Model): + """ + Shared permissions between linked copies of same dataset. + + Attaches a set of EditorUserPermission objects to a set of CatalogRecords. + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + +class PermissionRole(models.TextChoices): + """Permission role for EditorPermission.""" + + CREATOR = "creator" + EDITOR = "editor" + + +class EditorUserPermission(Common): + """Table for attaching user roles to an EditorPermissions object.""" + + # Override inherited integer based id with uuid + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + # MODEL FIELD DEFINITIONS # + editor_permissions = models.ForeignKey( + EditorPermissions, related_name="users", on_delete=models.CASCADE + ) + user_id = models.CharField(max_length=200) + role = models.CharField(max_length=16, choices=PermissionRole.choices) + + class Meta: + indexes = [ + models.Index( + fields=[ + "user_id", + ] + ), + ] + constraints = [ + models.UniqueConstraint( + fields=["editor_permissions", "user_id"], name="unique_dataset_user_permission" + ), + models.CheckConstraint(check=~models.Q(user_id=""), name="require_user_id"), + models.CheckConstraint( + check=models.Q(role__in=PermissionRole.values), name="require_role" + ), + ] + + def __repr__(self): + return f"" + + def delete(self, *args, **kwargs): + super().remove(*args, **kwargs) + + class DiscardRecord(Exception): pass @@ -445,6 +504,10 @@ class CatalogRecord(Common): help_text="Saves api related info about the dataset. E.g. api version", ) + editor_permissions = models.ForeignKey( + EditorPermissions, related_name="catalog_records", null=False, on_delete=models.PROTECT + ) + # END OF MODEL FIELD DEFINITIONS # """ @@ -1252,6 +1315,12 @@ def preservation_dataset_origin_version_exists(self): """ return hasattr(self, "preservation_dataset_origin_version") + def catalog_publishes_to_etsin(self): + return self.data_catalog.publish_to_etsin + + def catalog_publishes_to_ttv(self): + return self.data_catalog.publish_to_ttv + def catalog_versions_datasets(self): return self.data_catalog.catalog_json.get("dataset_versioning", False) is True @@ -1392,16 +1461,24 @@ def _pre_create_operations(self, pid_type=None): elif self.catalog_is_legacy(): if "preferred_identifier" not in self.research_dataset: - raise ValidationError( - { - "detail": [ - "Selected catalog %s is a legacy catalog. Preferred identifiers are not " - "automatically generated for datasets stored in legacy catalogs, nor is " - "their uniqueness enforced. Please provide a value for dataset field " - "preferred_identifier." % self.data_catalog.catalog_json["identifier"] - ] - } - ) + + # Reportronic catalog does not need to validate unique identifiers + # Raise validation error when not reportronic catalog + if ( + self.data_catalog.catalog_json["identifier"] + != settings.REPORTRONIC_DATA_CATALOG_IDENTIFIER + ): + raise ValidationError( + { + "detail": [ + "Selected catalog %s is a legacy catalog. Preferred identifiers are not " + "automatically generated for datasets stored in legacy catalogs, nor is " + "their uniqueness enforced. Please provide a value for dataset field " + "preferred_identifier." + % self.data_catalog.catalog_json["identifier"] + ] + } + ) _logger.info( "Catalog %s is a legacy catalog - not generating pid" % self.data_catalog.catalog_json["identifier"] @@ -1442,7 +1519,10 @@ def _pre_create_operations(self, pid_type=None): if "remote_resources" in self.research_dataset: self._calculate_total_remote_resources_byte_size() - if not ("files" in self.research_dataset or "directories" in self.research_dataset) and "total_files_byte_size" in self.research_dataset: + if ( + not ("files" in self.research_dataset or "directories" in self.research_dataset) + and "total_files_byte_size" in self.research_dataset + ): self.research_dataset.pop("total_files_byte_size") if self.cumulative_state == self.CUMULATIVE_STATE_CLOSED: @@ -1458,6 +1538,12 @@ def _pre_create_operations(self, pid_type=None): self._set_api_version() + # only new datasets need new EditorPermissions, copies already have one + if not self.editor_permissions_id: + self._add_editor_permissions() + if self.metadata_provider_user: + self._add_creator_editor_user_permission() + def _post_create_operations(self): if "files" in self.research_dataset or "directories" in self.research_dataset: # files must be added after the record itself has been created, to be able @@ -1491,7 +1577,7 @@ def _post_create_operations(self): or self.use_doi_for_published is True ): self._validate_cr_against_datacite_schema() - if is_metax_generated_doi_identifier(self.research_dataset["preferred_identifier"]): + if is_metax_generated_doi_identifier(self.research_dataset.get("preferred_identifier")): self.add_post_request_callable( DataciteDOIUpdate(self, self.research_dataset["preferred_identifier"], "create") ) @@ -3033,6 +3119,27 @@ def _copy_undeleted_files_from_old_version(self): if DEBUG: _logger.debug("Added %d files to dataset %s" % (n_files_copied, self._new_version.id)) + def _add_editor_permissions(self): + permissions = EditorPermissions.objects.create() + self.editor_permissions = permissions + + def _add_creator_editor_user_permission(self): + """ + Add creator permission to a newly created CatalogRecord. + """ + perm = EditorUserPermission( + editor_permissions=self.editor_permissions, + user_id=self.metadata_provider_user, + role=PermissionRole.CREATOR, + date_created=self.date_created, + date_modified=self.date_modified, + user_created=self.user_created, + user_modified=self.user_modified, + service_created=self.service_created, + service_modified=self.service_modified, + ) + perm.save() + class RabbitMQPublishRecord: @@ -3057,11 +3164,6 @@ def __call__(self): """ from metax_api.services import RabbitMQService as rabbitmq - _logger.info( - "Publishing CatalogRecord %s to RabbitMQ... routing_key: %s" - % (self.cr.identifier, self.routing_key) - ) - if self.routing_key == "delete": cr_json = {"identifier": self.cr.identifier} else: @@ -3070,9 +3172,27 @@ def __call__(self): cr_json["data_catalog"] = {"catalog_json": self.cr.data_catalog.catalog_json} try: - for exchange in settings.RABBITMQ["EXCHANGES"]: - if exchange["EXC_TYPE"] == "dataset": - rabbitmq.publish(cr_json, routing_key=self.routing_key, exchange=exchange["NAME"]) + if self.cr.catalog_publishes_to_etsin(): + + _logger.info( + "Publishing CatalogRecord %s to RabbitMQ... exchange: datasets, routing_key: %s" + % (self.cr.identifier, self.routing_key) + ) + + rabbitmq.publish( + cr_json, routing_key=self.routing_key, exchange="datasets" + ) + if self.cr.catalog_publishes_to_ttv(): + + _logger.info( + "Publishing CatalogRecord %s to RabbitMQ... exchange: ttv-datasets, routing_key: %s" + % (self.cr.identifier, self.routing_key) + ) + + rabbitmq.publish( + cr_json, routing_key=self.routing_key, exchange="ttv-datasets" + ) + except: # note: if we'd like to let the request be a success even if this operation fails, # we could simply not raise an exception here. diff --git a/src/metax_api/models/catalog_record_v2.py b/src/metax_api/models/catalog_record_v2.py index b9ee5895..583a75c2 100755 --- a/src/metax_api/models/catalog_record_v2.py +++ b/src/metax_api/models/catalog_record_v2.py @@ -117,6 +117,12 @@ def _pre_create_operations(self): self._set_api_version() + # only new datasets need new EditorPermissions, copies already have one + if not self.editor_permissions_id: + self._add_editor_permissions() + if self.metadata_provider_user: + self._add_creator_editor_user_permission() + def _post_create_operations(self, pid_type=None): if "files" in self.research_dataset or "directories" in self.research_dataset: diff --git a/src/metax_api/models/data_catalog.py b/src/metax_api/models/data_catalog.py index 37e2c100..bd1b5a86 100755 --- a/src/metax_api/models/data_catalog.py +++ b/src/metax_api/models/data_catalog.py @@ -59,6 +59,13 @@ class DataCatalog(Common): help_text="Services which are allowed to read catalog records in the catalog.", ) + publish_to_etsin = models.BooleanField( + default=True + ) + publish_to_ttv = models.BooleanField( + default=True + ) + # END OF MODEL FIELD DEFINITIONS # READ_METHODS = ("GET", "HEAD", "OPTIONS") diff --git a/src/metax_api/models/directory.py b/src/metax_api/models/directory.py index 5b8c1714..bc2f68fd 100755 --- a/src/metax_api/models/directory.py +++ b/src/metax_api/models/directory.py @@ -8,7 +8,7 @@ import logging from django.db import connection, models -from django.db.models import Count, Prefetch, Sum +from django.db.models import Count, Prefetch, Sum, Q from .common import Common from .file import File @@ -71,23 +71,24 @@ def calculate_byte_size_and_file_count(self): ) update_statements = [] + annotated_root_directory = self._get_project_directory_tree(with_own_sizes=True) + annotated_root_directory._calculate_byte_size_and_file_count(update_statements) + + if len(update_statements) > 0: + sql_update_all_directories = """ + update metax_api_directory as d set + byte_size = results.byte_size, + file_count = results.file_count + from (values + %s + ) as results(byte_size, file_count, id) + where results.id = d.id; + """ % ",".join( + update_statements + ) - self._calculate_byte_size_and_file_count(update_statements) - - sql_update_all_directories = """ - update metax_api_directory as d set - byte_size = results.byte_size, - file_count = results.file_count - from (values - %s - ) as results(byte_size, file_count, id) - where results.id = d.id; - """ % ",".join( - update_statements - ) - - with connection.cursor() as cursor: - cursor.execute(sql_update_all_directories) + with connection.cursor() as cursor: + cursor.execute(sql_update_all_directories) _logger.info( "Project %s directory tree calculations complete. Total byte_size: " @@ -100,39 +101,59 @@ def calculate_byte_size_and_file_count(self): ) ) + def _get_project_directory_tree(self, with_own_sizes=False): + """ + Get all project directories from DB in single query. Returns current directory with + subdirectories in directory.sub_dirs. + + Optionally, annotate directories with total size and + count of files they contain as own_byte_size and own_file_count. + """ + project_directories = Directory.objects.filter( + project_identifier=self.project_identifier + ).only("file_count", "byte_size", "parent_directory_id") + + if with_own_sizes: + project_directories = project_directories.annotate( + own_byte_size=Sum("files__byte_size", filter=Q(files__removed=False)), + own_file_count=Count("files", filter=Q(files__removed=False)), + ) + + # build directory tree from annotated directories + directories_by_id = {d.id: d for d in project_directories} + for d in project_directories: + d.sub_dirs = [] + for d in project_directories: + if d.parent_directory_id is not None: + directories_by_id[d.parent_directory_id].sub_dirs.append(d) + annotated_root_directory = directories_by_id.get(self.id) + return annotated_root_directory + def _calculate_byte_size_and_file_count(self, update_statements): """ Recursively traverse the entire directory tree and update total byte size and file count for each directory. Accumulates a list of triplets for a big sql-update statement. """ + old_byte_size = self.byte_size + old_file_count = self.file_count self.byte_size = 0 self.file_count = 0 - # fields id, parent_directory_id must be specified for joining for Prefetch-object to work properly - sub_dirs = ( - self.child_directories.all() - .only("byte_size", "parent_directory_id") - .prefetch_related( - Prefetch( - "files", - queryset=File.objects.only("id", "byte_size", "parent_directory_id"), - ) - ) - ) - - if sub_dirs: - for sub_dir in sub_dirs: + if self.sub_dirs: + for sub_dir in self.sub_dirs: sub_dir._calculate_byte_size_and_file_count(update_statements) # sub dir numbers - self.byte_size = sum(d.byte_size for d in sub_dirs) - self.file_count = sum(d.file_count for d in sub_dirs) + self.byte_size = sum(d.byte_size for d in self.sub_dirs) + self.file_count = sum(d.file_count for d in self.sub_dirs) # note: never actually saved using .save() - self.byte_size += sum(f.byte_size for f in self.files.all()) or 0 - self.file_count += len(self.files.all()) or 0 + self.byte_size += self.own_byte_size or 0 + self.file_count += self.own_file_count or 0 - update_statements.append("(%d, %d, %d)" % (self.byte_size, self.file_count, self.id)) + # add updated values if changed + if self.byte_size != old_byte_size or self.file_count != old_file_count: + update_statements.append("(%d, %d, %d)" % (self.byte_size, self.file_count, self.id)) def calculate_byte_size_and_file_count_for_cr(self, cr_id, directory_data): """ @@ -156,7 +177,10 @@ def calculate_byte_size_and_file_count_for_cr(self, cr_id, directory_data): parent_id: (byte_size, file_count) for parent_id, byte_size, file_count in stats } - self._calculate_byte_size_and_file_count_for_cr(grouped_by_dir, directory_data) + directory_tree = self._get_project_directory_tree() + directory_tree._calculate_byte_size_and_file_count_for_cr( + grouped_by_dir, directory_data + ) def _calculate_byte_size_and_file_count_for_cr(self, grouped_by_dir, directory_data): """ @@ -173,15 +197,13 @@ def _calculate_byte_size_and_file_count_for_cr(self, grouped_by_dir, directory_d self.byte_size = 0 self.file_count = 0 - sub_dirs = self.child_directories.all().only("id") - - if sub_dirs: - for sub_dir in sub_dirs: + if self.sub_dirs: + for sub_dir in self.sub_dirs: sub_dir._calculate_byte_size_and_file_count_for_cr(grouped_by_dir, directory_data) # sub dir numbers - self.byte_size = sum(d.byte_size for d in sub_dirs) - self.file_count = sum(d.file_count for d in sub_dirs) + self.byte_size = sum(d.byte_size for d in self.sub_dirs) + self.file_count = sum(d.file_count for d in self.sub_dirs) current_dir = grouped_by_dir.get(self.id, [0, 0]) diff --git a/src/metax_api/services/catalog_record_service.py b/src/metax_api/services/catalog_record_service.py index 3f144272..73277998 100755 --- a/src/metax_api/services/catalog_record_service.py +++ b/src/metax_api/services/catalog_record_service.py @@ -130,8 +130,28 @@ def get_queryset_search_params(cls, request): queryset_search_params["api_meta__contains"] = {"version": value} + if request.query_params.get("editor_permissions_user"): + cls.filter_by_editor_permissions_user(request, queryset_search_params) + return queryset_search_params + @staticmethod + def filter_by_editor_permissions_user(request, queryset_search_params): + """ + Add filter for querying datasets where user has editor user permissions. + """ + user_id = request.query_params["editor_permissions_user"] + + # non-service users can only query their own datasets + if not request.user.is_service: + if request.user.username == '': + raise Http403({"detail": ["Query by editor_permissions_user is only supported for authenticated users"]}) + if request.user.username != user_id: + raise Http403({"detail": ["Provided editor_permissions_user does not match current user"]}) + + queryset_search_params["editor_permissions__users__user_id"] = user_id + queryset_search_params["editor_permissions__users__removed"] = False + @staticmethod def filter_by_state(request, queryset_search_params): """ diff --git a/src/metax_api/services/file_service.py b/src/metax_api/services/file_service.py index ad2d9617..ad0c983e 100755 --- a/src/metax_api/services/file_service.py +++ b/src/metax_api/services/file_service.py @@ -208,7 +208,8 @@ def restore_files(cls, request, file_identifier_list): removed = false, file_deleted = NULL, date_modified = CURRENT_TIMESTAMP, - user_modified = NULL + user_modified = NULL, + date_removed = NULL from (values %s ) as results(id, service_modified, parent_directory_id) @@ -440,8 +441,11 @@ def _mark_files_as_deleted(file_ids): _logger.info("Marking files as removed...") sql_delete_files = """ - update metax_api_file - set removed = true, file_deleted = CURRENT_TIMESTAMP, date_modified = CURRENT_TIMESTAMP + update metax_api_file set + removed = true, + file_deleted = CURRENT_TIMESTAMP, + date_modified = CURRENT_TIMESTAMP, + date_removed = CURRENT_TIMESTAMP where active = true and removed = false and id in %s""" diff --git a/src/metax_api/services/rabbitmq_service.py b/src/metax_api/services/rabbitmq_service.py index a4192717..bd7daba9 100755 --- a/src/metax_api/services/rabbitmq_service.py +++ b/src/metax_api/services/rabbitmq_service.py @@ -209,10 +209,11 @@ class _RabbitMQServiceDummy: """ def __init__(self, settings=settings): - pass + self.messages = [] def publish(self, body, routing_key="", exchange="datasets", persistent=True): - pass + msg = {"body":body, "routing_key":routing_key, "exchange":exchange, "persistent":persistent} + self.messages.append(msg) def init_exchanges(self, *args, **kwargs): pass diff --git a/src/metax_api/services/rems_service.py b/src/metax_api/services/rems_service.py index ffcbeb9f..766d6f8b 100755 --- a/src/metax_api/services/rems_service.py +++ b/src/metax_api/services/rems_service.py @@ -39,6 +39,7 @@ def __init__(self): self.reporter_user = settings["REPORTER_USER"] self.auto_approver = settings["AUTO_APPROVER"] self.form_id = settings["FORM_ID"] + self.organization = settings["ORGANIZATION"] self.headers = { "x-rems-api-key": self.api_key, @@ -60,6 +61,9 @@ def create_rems_entity(self, cr, user_info): """ self.cr = cr + # raise error if configured organization does not exist in REMS + self._get_rems_organization() + # create user. Successful even if userid is already taken self._post_rems("user", user_info) @@ -112,6 +116,12 @@ def _get_catalogue_item(self, rems_id): return rems_ci + def _get_rems_organization(self): + """ + Get configured organization object from REMS. + """ + return self._get_rems("organization", id=self.organization) + def _close_applications(self, rems_id, reason): """ Get all applications that are related to dataset and close them. @@ -153,7 +163,7 @@ def _close_entity(self, entity, id): def _create_workflow(self, user_id): body = { - "organization": self.cr.metadata_owner_org, + "organization": {"organization/id": self.organization}, "title": self.cr.research_dataset["preferred_identifier"], "type": "workflow/default", "handlers": [user_id], @@ -176,7 +186,11 @@ def _create_license(self): if any([v["textcontent"] == license_url for v in lic["localizations"].values()]): return lic["id"] - body = {"licensetype": "link", "localizations": {}} + body = { + "licensetype": "link", + "localizations": {}, + "organization": {"organization/id": self.organization}, + } for lang in list(license["title"].keys()): body["localizations"].update( @@ -190,7 +204,7 @@ def _create_license(self): def _create_resource(self, license_id): body = { "resid": self.cr.rems_identifier, - "organization": self.cr.metadata_owner_org, + "organization": {"organization/id": self.organization}, "licenses": [license_id], } @@ -207,6 +221,7 @@ def _create_catalogue_item(self, res_id, wf_id): "wfid": wf_id, "localizations": {}, "enabled": True, + "organization": {"organization/id": self.organization}, } for lang in list(rd_title.keys()): @@ -273,9 +288,17 @@ def _put_rems(self, entity, action, body): return resp - def _get_rems(self, entity, params=""): + def _get_rems(self, entity, params="", id=None): + """Get list of REMS entities or single entity by id. """ + if id: + id_path = f"/{id}" + else: + id_path = "" + try: - response = requests.get(f"{self.base_url}/{entity}s?{params}", headers=self.headers) + response = requests.get( + f"{self.base_url}/{entity}s{id_path}?{params}", headers=self.headers + ) except Exception as e: raise Exception(f"Connection to REMS failed while getting {entity}. Error: {e}") diff --git a/src/metax_api/services/statistic_service.py b/src/metax_api/services/statistic_service.py index 21d547e7..ac052619 100755 --- a/src/metax_api/services/statistic_service.py +++ b/src/metax_api/services/statistic_service.py @@ -131,13 +131,7 @@ def count_datasets( if legacy is not None: where_args.append( - "".join( - [ - "and dc.catalog_json->>'identifier'", - " = " if legacy else " != ", - "any(%s)", - ] - ) + ''.join(["and", " " if legacy else " NOT ", "dc.catalog_json->>'identifier'", " = ", "any(%s)"]) ) sql_args.append(settings.LEGACY_CATALOGS) @@ -210,13 +204,7 @@ def total_datasets(cls, from_date, to_date, latest=True, legacy=None, removed=No if legacy is not None: filter_sql.append( - "".join( - [ - "and dc.catalog_json->>'identifier'", - " = " if legacy else " != ", - "any(%s)", - ] - ) + ''.join(["and", " " if legacy else " NOT ", "dc.catalog_json->>'identifier'", " = ", "any(%s)"]) ) filter_args.append(settings.LEGACY_CATALOGS) @@ -345,7 +333,7 @@ def _total_data_catalog_datasets(cls, from_date, to_date, access_types, dc_id): return grouped @classmethod - def total_organization_datasets(cls, from_date, to_date, metadata_owner_org=None): + def total_organization_datasets(cls, from_date, to_date, metadata_owner_org=None, latest=True, legacy=None, removed=None): """ Retrieve dataset count and byte size per month and monthly cumulative for all organizations, or a given single organization, grouped by catalog. @@ -366,12 +354,12 @@ def total_organization_datasets(cls, from_date, to_date, metadata_owner_org=None results = {} for org in metadata_owner_orgs: - results[org] = cls._total_organization_datasets(from_date, to_date, org) + results[org] = cls._total_organization_datasets(from_date, to_date, org, latest, legacy, removed) return results @classmethod - def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): + def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org, latest, legacy, removed): sql = """ WITH cte AS ( SELECT @@ -382,6 +370,7 @@ def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): where dc.id = %s and state = 'published' and cr.metadata_owner_org = %s + OPTIONAL_WHERE_FILTERS GROUP BY mon ) SELECT @@ -403,12 +392,37 @@ def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): where dc.id = %s and state = 'published' and cr.metadata_owner_org = %s + OPTIONAL_WHERE_FILTERS GROUP BY mon ) cr USING (mon) GROUP BY mon, c.mon_ida_byte_size, count ORDER BY mon; """ + filter_sql = [] + filter_args = [] + + if latest: + filter_sql.append("and next_dataset_version_id is null") + + if removed is not None: + filter_sql.append("and cr.removed = %s") + filter_args.append(removed) + + if legacy is not None: + filter_sql.append( + "".join( + [ + "AND ", + "" if legacy else "NOT ", + "dc.catalog_json->>'identifier' = ANY(%s)", + ] + ) + ) + filter_args.append(settings.LEGACY_CATALOGS) + + sql = sql.replace("OPTIONAL_WHERE_FILTERS", "\n".join(filter_sql)) + catalogs = DataCatalog.objects.filter( catalog_json__research_dataset_schema__in=["ida", "att"] ).values() @@ -418,17 +432,8 @@ def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): with connection.cursor() as cr: for dc in catalogs: - cr.execute( - sql, - [ - dc["id"], - metadata_owner_org, - from_date, - to_date, - dc["id"], - metadata_owner_org, - ], - ) + sql_args = [dc["id"], metadata_owner_org] + filter_args + [from_date, to_date, dc["id"], metadata_owner_org] + filter_args + cr.execute(sql, sql_args) results = [ dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall() ] diff --git a/src/metax_api/settings/components/access_control.py b/src/metax_api/settings/components/access_control.py index 387c0c90..ebd99fd4 100755 --- a/src/metax_api/settings/components/access_control.py +++ b/src/metax_api/settings/components/access_control.py @@ -11,6 +11,7 @@ "datacatalogs": {}, "datasets": {}, "directories": {}, + "editorpermissions": {}, "files": {}, "filestorages": {}, "schemas": {}, @@ -36,6 +37,10 @@ class Role(Enum): API_AUTH_USER = "api_auth_user" EXTERNAL = "external" JYU = "jyu" + REPORTRONIC = "reportronic" + AALTO = "aalto" + EUDAT = "eudat" + SD = "sd" def __ge__(self, other): if self.__class__ is other.__class__: @@ -74,6 +79,8 @@ def __lt__(self, other): Role.TPAS, Role.QVAIN, Role.ETSIN, + Role.EUDAT, + Role.JYU, ] api_permissions.rest.datasets.read = [Role.ALL] api_permissions.rest.datasets["update"] = [ @@ -91,6 +98,32 @@ def __lt__(self, other): Role.ETSIN, ] +api_permissions.rest.editorpermissions.create = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.ETSIN, +] +api_permissions.rest.editorpermissions.read = [Role.ALL] +api_permissions.rest.editorpermissions["update"] = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.ETSIN, +] +api_permissions.rest.editorpermissions.delete = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.ETSIN, +] + api_permissions.rest.directories.read = [ Role.METAX, Role.QVAIN, @@ -132,6 +165,7 @@ def __lt__(self, other): api_permissions.rpc.datasets.create_draft.use = [Role.ALL] api_permissions.rpc.datasets.create_new_version.use = [Role.ALL] api_permissions.rpc.datasets.fix_deprecated.use = [Role.ALL] +api_permissions.rpc.datasets.flush_user_data.use = [Role.METAX, Role.IDA, Role.TPAS] api_permissions.rpc.datasets.get_minimal_dataset_template.use = [Role.ALL] api_permissions.rpc.datasets.merge_draft.use = [Role.ALL] api_permissions.rpc.datasets.publish_dataset.use = [Role.ALL] diff --git a/src/metax_api/settings/components/common.py b/src/metax_api/settings/components/common.py index e422aeda..fcc0f34e 100755 --- a/src/metax_api/settings/components/common.py +++ b/src/metax_api/settings/components/common.py @@ -6,11 +6,16 @@ DEBUG = env("DEBUG") SECRET_KEY = env("DJANGO_SECRET_KEY") ADDITIONAL_USER_PROJECTS_PATH = env("ADDITIONAL_USER_PROJECTS_PATH") + IDA_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-ida" ATT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-att" PAS_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-pas" LEGACY_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-legacy" DFT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-dft" +REPORTRONIC_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-reportronic" +AALTO_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-acris" +FMI_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-fmi" +SD_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-sd" END_USER_ALLOWED_DATA_CATALOGS = [ IDA_DATA_CATALOG_IDENTIFIER, @@ -23,7 +28,9 @@ # catalogs where uniqueness of dataset pids is not enforced. LEGACY_CATALOGS = [ LEGACY_DATA_CATALOG_IDENTIFIER, + REPORTRONIC_DATA_CATALOG_IDENTIFIER, ] + VALIDATE_TOKEN_URL = env("VALIDATE_TOKEN_URL") CHECKSUM_ALGORITHMS = ["SHA-256", "MD5", "SHA-512"] ERROR_FILES_PATH = env("ERROR_FILES_PATH") @@ -139,10 +146,6 @@ DATABASES["default"]["ENGINE"] = "django.db.backends.postgresql" DATABASES["default"]["ATOMIC_REQUESTS"] = True -# Colorize automated test console output -RAINBOWTESTS_HIGHLIGHT_PATH = str(BASE_DIR) -TEST_RUNNER = "rainbowtests.test.runner.RainbowDiscoverRunner" - # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ @@ -183,7 +186,7 @@ if env("ENABLE_V2_ENDPOINTS"): API_VERSIONS_ENABLED.append("v2") -# Variables related to api credentials +# API credentials in development environment API_USERS = [ {"password": "test-metax", "username": "metax"}, {"password": "test-qvain", "username": "qvain"}, @@ -192,6 +195,9 @@ {"password": "test-etsin", "username": "etsin"}, {"password": "test-fds", "username": "fds"}, {"password": "test-download", "username": "download"}, + {"password": "test-eudat", "username": "eudat"}, + {"password": "test-jyu", "username": "jyu"}, + {"password": "test-sd", "username": "sd"}, ] SWAGGER_YAML_PATH = env('SWAGGER_YAML_PATH') diff --git a/src/metax_api/settings/components/logging.py b/src/metax_api/settings/components/logging.py index 93637a14..1d9e2e79 100755 --- a/src/metax_api/settings/components/logging.py +++ b/src/metax_api/settings/components/logging.py @@ -63,7 +63,7 @@ "metax_api": { "handlers": ["general", "console", "debug"], }, - "root": {"level": LOGGING_LEVEL, "handlers": ["console"]}, + "root": {"level": LOGGING_LEVEL, "handlers": ["console", "general"]}, }, } diff --git a/src/metax_api/settings/components/rems.py b/src/metax_api/settings/components/rems.py index 810c1ec7..abbcd718 100644 --- a/src/metax_api/settings/components/rems.py +++ b/src/metax_api/settings/components/rems.py @@ -19,5 +19,6 @@ REMS["REPORTER_USER"] = env("REMS_REPORTER_USER") REMS["AUTO_APPROVER"] = env("REMS_AUTO_APPROVER") REMS["FORM_ID"] = int(env("REMS_FORM_ID")) + REMS["ORGANIZATION"] = env("REMS_ORGANIZATION") except ImproperlyConfigured as e: logger.warning(e) diff --git a/src/metax_api/settings/environments/production.py b/src/metax_api/settings/environments/production.py index cf5223cb..30c9b856 100644 --- a/src/metax_api/settings/environments/production.py +++ b/src/metax_api/settings/environments/production.py @@ -1,6 +1,8 @@ from metax_api.settings.environments.stable import api_permissions, prepare_perm_values from metax_api.settings.environments.staging import API_USERS # noqa: F401 +api_permissions.rpc.datasets.flush_user_data.use.clear() + api_permissions.rpc.files.flush_project.use.clear() API_ACCESS = prepare_perm_values(api_permissions.to_dict()) diff --git a/src/metax_api/settings/environments/stable.py b/src/metax_api/settings/environments/stable.py index 2f59d398..b5c57b3a 100644 --- a/src/metax_api/settings/environments/stable.py +++ b/src/metax_api/settings/environments/stable.py @@ -1,9 +1,9 @@ from metax_api.settings.components.access_control import Role, api_permissions, prepare_perm_values from metax_api.settings.environments.staging import API_USERS # noqa: F401 -api_permissions.rest.datasets.create += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU] -api_permissions.rest.datasets["update"] += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU] -api_permissions.rest.datasets.delete += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU] +api_permissions.rest.datasets.create += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU, Role.REPORTRONIC, Role.AALTO, Role.EUDAT, Role.SD] +api_permissions.rest.datasets["update"] += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU, Role.REPORTRONIC, Role.AALTO, Role.EUDAT, Role.SD] +api_permissions.rest.datasets.delete += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU, Role.REPORTRONIC, Role.AALTO, Role.EUDAT, Role.SD] api_permissions.rest.directories.read += [Role.IDA, Role.QVAIN_LIGHT] diff --git a/src/metax_api/settings/environments/staging.py b/src/metax_api/settings/environments/staging.py index e9c1675a..88ca063e 100644 --- a/src/metax_api/settings/environments/staging.py +++ b/src/metax_api/settings/environments/staging.py @@ -31,6 +31,7 @@ api_permissions.rest.filestorages.delete = [Role.ALL] api_permissions.rpc.datasets.set_preservation_identifier.use = [Role.ALL] +api_permissions.rpc.datasets.flush_user_data.use = [Role.ALL] api_permissions.rpc.files.delete_project.use = [Role.ALL] api_permissions.rpc.files.flush_project.use = [Role.ALL] diff --git a/src/metax_api/settings/environments/unittests.py b/src/metax_api/settings/environments/unittests.py index 631d9182..e6950902 100755 --- a/src/metax_api/settings/environments/unittests.py +++ b/src/metax_api/settings/environments/unittests.py @@ -58,3 +58,19 @@ api_permissions.rpc.files.delete_project.use += [Role.TEST_USER] API_ACCESS = prepare_perm_values(api_permissions.to_dict()) + +from metax_api.settings.components.rems import REMS + +REMS.update( + { + "ENABLED": True, + "API_KEY": "key", + "BASE_URL": "https://mock-rems/api", + "ETSIN_URL_TEMPLATE": "https://etsin.fd-dev.csc.fi/dataset/%s", + "METAX_USER": "rems-metax@example.com", + "REPORTER_USER": "rems-reporter@example.com", + "AUTO_APPROVER": "not-used", + "FORM_ID": 1, + "ORGANIZATION": "rems-test-org", + } +) diff --git a/src/metax_api/swagger/v1/swagger.yaml b/src/metax_api/swagger/v1/swagger.yaml index 9c76ecc8..cb605be1 100755 --- a/src/metax_api/swagger/v1/swagger.yaml +++ b/src/metax_api/swagger/v1/swagger.yaml @@ -640,7 +640,7 @@ paths: in: query description: Sets paging on with default limit of 10 required: false - type: bolean + type: boolean - name: offset in: query description: Offset for paging @@ -1414,8 +1414,133 @@ paths: description: Resource not found. tags: - Dataset API - - + /rest/datasets/{CRID}/editor_permissions/users: + get: + summary: List all editor permissions of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + responses: + "200": + description: Successful operation, return a list of editor rights. May return an empty list. + schema: + type: array + items: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + post: + summary: Create a new editor permission of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: body + in: body + schema: + type: object + properties: + user_id: + type: string + role: + type: string + responses: + "201": + description: Successful operation, return created editor rights. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + /rest/datasets/{CRID}/editor_permissions/users/{USER_ID}: + get: + summary: List all editor permissions of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + responses: + "200": + description: Successful operation, return a list of editor rights. May return an empty list. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + patch: + summary: Update role or enable verified + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + - name: body + in: body + schema: + $ref: '#/definitions/EditorUserPermission' + responses: + "200": + description: Successful operation, return changed editor rights. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + delete: + summary: Editorpermission marked as removed + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + responses: + '200': + description: Successful operation + '400': + description: Bad request + '403': + description: Forbidden + '404': + description: Not found + tags: + - Dataset API # Contract API /rest/contracts: get: @@ -1695,6 +1820,25 @@ paths: description: Successful operation, returns information about the new dataset version. tags: - Dataset RPC + /rpc/datasets/flush_user_data: + post: + summary: Flushes user's datasets + description: Usable by ida, tpas and metax users. Permanently deletes datasets created by a certain user. Only for non-production use. + parameters: + - name: metadata_provider_user + in: query + description: Identifies the user whose datasets will be flushed. + required: true + type: string + responses: + '204': + description: Successful operation, returns no content. + '400': + description: Required parameter was missing or API was called in production environment. Response includes details. + '404': + description: No datasets were found for requested user. + tags: + - Dataset RPC /rpc/statistics/count_datasets: get: summary: Get the total number of datasets and file sizes. @@ -1830,6 +1974,9 @@ paths: - $ref: "#/parameters/from_date" - $ref: "#/parameters/to_date" - $ref: "#/parameters/metadata_owner_org_filter" + - $ref: "#/parameters/latest_filter" + - $ref: "#/parameters/removed_filter" + - $ref: "#/parameters/legacy_filter" responses: '200': description: Successful operation. @@ -1867,6 +2014,25 @@ paths: description: Unsuccesful operation when project identifier is missing. tags: - File RPC + /rpc/files/flush_project: + post: + summary: Flushes given project from the database. + description: Usable by ida, tpas and metax users. Permanently deletes given project's files and directories. Only for non-production use. + parameters: + - name: project_identifier + in: query + description: Identifies the project whose files and directories will be flushed. + required: true + type: string + responses: + '204': + description: Successful operation, returns no content. + '400': + description: Required parameter was missing or API was called in production environment. Response includes details. + '404': + description: No files or directories were found for requested project. + tags: + - File RPC parameters: @@ -2043,8 +2209,43 @@ definitions: count_cumulative: type: number format: float - - + EditorUserPermission: + type: object + properties: + id: + type: integer + readOnly: true + active: + type: boolean + removed: + type: boolean + date_modified: + type: string + format: date-time + readOnly: true + user_modified: + type: string + date_created: + type: string + format: date-time + readOnly: true + user_created: + type: string + service_modified: + type: string + service_created: + type: string + date_removed: + type: string + format: date-time + user_id: + type: string + role: + type: string + verified: + type: boolean + editor_permission_id: + type: integer examples: count_datasets: type: object diff --git a/src/metax_api/swagger/v2/swagger.yaml b/src/metax_api/swagger/v2/swagger.yaml index 682ffdf8..51a88991 100755 --- a/src/metax_api/swagger/v2/swagger.yaml +++ b/src/metax_api/swagger/v2/swagger.yaml @@ -1598,7 +1598,133 @@ paths: description: Resource not found. tags: - Dataset API - + /rest/datasets/{CRID}/editor_permissions/users: + get: + summary: List all editor permissions of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + responses: + "200": + description: Successful operation, return a list of editor rights. May return an empty list. + schema: + type: array + items: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + post: + summary: Create a new editor permission of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: body + in: body + schema: + type: object + properties: + user_id: + type: string + role: + type: string + responses: + "201": + description: Successful operation, return created editor rights. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + /rest/datasets/{CRID}/editor_permissions/users/{USER_ID}: + get: + summary: List all editor permissions of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + responses: + "200": + description: Successful operation, return a list of editor rights. May return an empty list. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + patch: + summary: Update role + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + - name: body + in: body + schema: + $ref: '#/definitions/EditorUserPermission' + responses: + "200": + description: Successful operation, return changed editor rights. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + delete: + summary: Editorpermission marked as removed + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + responses: + '200': + description: Successful operation + '400': + description: Bad request + '403': + description: Forbidden + '404': + description: Not found + tags: + - Dataset API # Contract API /rest/v2/contracts: @@ -1974,6 +2100,25 @@ paths: description: Not found tags: - Dataset RPC + /rpc/v2/datasets/flush_user_data: + post: + summary: Flushes user's datasets + description: Usable by ida, tpas and metax users. Permanently deletes datasets created by a certain user. Only for non-production use. + parameters: + - name: metadata_provider_user + in: query + description: Identifies the user whose datasets will be flushed. + required: true + type: string + responses: + '204': + description: Successful operation, returns no content. + '400': + description: Required parameter was missing or API was called in production environment. Response includes details. + '404': + description: No datasets were found for requested user. + tags: + - Dataset RPC /rpc/v2/statistics/count_datasets: get: summary: Get the total number of datasets and file sizes. @@ -2109,6 +2254,9 @@ paths: - $ref: "#/parameters/from_date" - $ref: "#/parameters/to_date" - $ref: "#/parameters/metadata_owner_org_filter" + - $ref: "#/parameters/latest_filter" + - $ref: "#/parameters/removed_filter" + - $ref: "#/parameters/legacy_filter" responses: '200': description: Successful operation. @@ -2146,6 +2294,25 @@ paths: description: Unsuccesful operation when project identifier is missing. tags: - File RPC + /rpc/v2/files/flush_project: + post: + summary: Flushes given project from the database. + description: Usable by ida, tpas and metax users. Permanently deletes given project's files and directories. Only for non-production use. + parameters: + - name: project_identifier + in: query + description: Identifies the project whose files and directories will be flushed. + required: true + type: string + responses: + '204': + description: Successful operation, returns no content. + '400': + description: Required parameter was missing or API was called in production environment. Response includes details. + '404': + description: No files or directories were found for requested project. + tags: + - File RPC parameters: @@ -2389,7 +2556,41 @@ definitions: count_cumulative: type: number format: float - + EditorUserPermission: + type: object + properties: + id: + type: integer + readOnly: true + active: + type: boolean + removed: + type: boolean + date_modified: + type: string + format: date-time + readOnly: true + user_modified: + type: string + date_created: + type: string + format: date-time + readOnly: true + user_created: + type: string + service_modified: + type: string + service_created: + type: string + date_removed: + type: string + format: date-time + user_id: + type: string + role: + type: string + editor_permission_id: + type: integer examples: count_datasets: diff --git a/src/metax_api/tests/api/rest/base/views/common/read.py b/src/metax_api/tests/api/rest/base/views/common/read.py index 6d4887d7..779508ca 100755 --- a/src/metax_api/tests/api/rest/base/views/common/read.py +++ b/src/metax_api/tests/api/rest/base/views/common/read.py @@ -142,7 +142,6 @@ def test_pagination_ordering(self): self.assertEqual(from_api, from_db) - class ApiReadHTTPHeaderTests(CatalogRecordApiReadCommon): # # header if-modified-since tests, single @@ -193,7 +192,7 @@ def test_get_with_if_modified_since_header_syntax_error(self): # List operation returns always 200 even if no datasets match the if-modified-since criterium - def test_list_get_with_if_modified_since_header_ok(self): + def _test_list_get_with_if_modified_since_header_ok(self): cr = CatalogRecord.objects.get(pk=self.pk) date_modified = cr.date_modified date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz("GMT")) @@ -234,6 +233,16 @@ def test_list_get_with_if_modified_since_header_ok(self): self.assertTrue(len(response.data.get("results")) > 6) self.assertTrue(len(response.data.get("results")) == 28) + def test_list_get_with_if_modified_since_header_ok_as_unauthenticated_user(self): + + self._set_http_authorization("no") + self._test_list_get_with_if_modified_since_header_ok() + + def test_list_get_with_if_modified_since_header_ok_as_authenticated_user(self): + + self._use_http_authorization() + self._test_list_get_with_if_modified_since_header_ok() + class ApiReadQueryParamTests(CatalogRecordApiReadCommon): diff --git a/src/metax_api/tests/api/rest/base/views/datasets/read.py b/src/metax_api/tests/api/rest/base/views/datasets/read.py index 98b255bc..688e99f2 100755 --- a/src/metax_api/tests/api/rest/base/views/datasets/read.py +++ b/src/metax_api/tests/api/rest/base/views/datasets/read.py @@ -965,6 +965,19 @@ def test_filter_by_legacy(self): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(count_all, response.data["count"], response.data) + def test_filter_by_editor_permissions_user_ok(self): + cr = CatalogRecord.objects.get(pk=1) + cr.editor_permissions.users.update(user_id='test_user_x') + response = self.client.get(f"/rest/datasets?editor_permissions_user=test_user_x") + self.assertEqual(response.data["count"], 1) + + def test_filter_by_editor_permissions_user_removed(self): + cr = CatalogRecord.objects.get(pk=1) + cr.editor_permissions.users.update(user_id='test_user_x') + cr.editor_permissions.users.first().delete() + response = self.client.get(f"/rest/datasets?editor_permissions_user=test_user_x") + self.assertEqual(response.data["count"], 0) + class CatalogRecordApiReadXMLTransformationTests(CatalogRecordApiReadCommon): @@ -1068,12 +1081,8 @@ def test_read_dataset_format_dummy_datacite_doi(self): def _check_dataset_xml_format_response(self, response, element_name): self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual("content-type" in response._headers, True, response._headers) - self.assertEqual( - "application/xml" in response._headers["content-type"][1], - True, - response._headers, - ) + + self.assertEqual(" +# :license: MIT + +from json import load as json_load +import uuid +import responses + +from django.core.management import call_command + +from rest_framework import status +from rest_framework.test import APITestCase + +from metax_api.tests.utils import TestClassUtils, test_data_file_path + + +class EditorUserPermissionApiReadCommon(APITestCase, TestClassUtils): + @classmethod + def setUpClass(cls): + """Loaded only once for test cases inside this class.""" + call_command("loaddata", test_data_file_path, verbosity=0) + super(EditorUserPermissionApiReadCommon, cls).setUpClass() + + def setUp(self): + self.cr_from_test_data = self._get_whole_object_from_test_data( + "catalogrecord", requested_pk=1 + ) + self.crid = self.cr_from_test_data["pk"] + self.identifier = "cr955e904-e3dd-4d7e-99f1-3fed446f96d1" + self.permissionid = self.cr_from_test_data["fields"]["editor_permissions_id"] + self.metadata_provider_user = self.cr_from_test_data["fields"]["metadata_provider_user"] + self.editor_user_permission = self._get_whole_object_from_test_data( + "editoruserpermission", requested_pk=str(uuid.UUID(int=1)) + ) + self.userid = self.editor_user_permission["fields"]["user_id"] + self._use_http_authorization() + + def _get_whole_object_from_test_data(self, model_name, requested_pk=0): + + with open(test_data_file_path) as test_data_file: + test_data = json_load(test_data_file) + + model = "metax_api.%s" % model_name + + for row in test_data: + if row["model"] == model: + if row["pk"] == requested_pk: + obj = { + "id": row["pk"], + } + obj.update(row) + return obj + + raise Exception( + "Could not find model %s from test data with pk == %d. " + "Are you certain you generated rows for model %s in generate_test_data.py?" + % (model_name, requested_pk, model_name) + ) + + +class EditorUserPermissionApiReadBasicTests(EditorUserPermissionApiReadCommon): + + """Basic read operations.""" + + def test_read_editor_permission_list_with_pk(self): + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_read_editor_permission_list_with_uuid(self): + response = self.client.get("/rest/datasets/%s/editor_permissions/users" % self.identifier) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_read_editor_permission_list_invalid(self): + response = self.client.get("/rest/datasets/99999/editor_permissions/users") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_read_editor_permission_details_by_pk(self): + response = self.client.get( + "/rest/datasets/%d/editor_permissions/users/%s" % (self.crid, self.userid) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(str(response.data["editor_permissions"]), self.permissionid) + self.assertEqual(response.data["user_id"], self.userid) + + def test_read_editor_permission_details_by_pk_invalid(self): + response = self.client.get( + "/rest/datasets/%d/editor_permissions/users/%s" % (self.crid, "invalid") + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + @responses.activate + def test_read_editor_permission_list_by_wrong_user(self): + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", token={"group_names": [], "CSCUserName": "not_dataset_creator"} + ) + response = self.client.get(f"/rest/datasets/{self.crid}/editor_permissions/users") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @responses.activate + def test_read_editor_permission_list_by_provider_user(self): + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", token={"group_names": [], "CSCUserName": self.metadata_provider_user} + ) + response = self.client.get(f"/rest/datasets/{self.crid}/editor_permissions/users") + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/metax_api/tests/api/rest/base/views/editorpermissions/write.py b/src/metax_api/tests/api/rest/base/views/editorpermissions/write.py new file mode 100644 index 00000000..99ccc858 --- /dev/null +++ b/src/metax_api/tests/api/rest/base/views/editorpermissions/write.py @@ -0,0 +1,219 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + + +from json import load as json_load +import uuid + +import responses +from django.core.management import call_command + +from rest_framework import status +from rest_framework.test import APITestCase + +from metax_api.models import CatalogRecord, EditorUserPermission +from metax_api.tests.utils import TestClassUtils, test_data_file_path + + +class EditorUserPermissionApiWriteCommon(APITestCase, TestClassUtils): + @classmethod + def setUpClass(cls): + """ + Loaded only once for test cases inside this class. + """ + call_command("loaddata", test_data_file_path, verbosity=0) + super(EditorUserPermissionApiWriteCommon, cls).setUpClass() + + def setUp(self): + self.cr_from_test_data = self._get_whole_object_from_test_data( + "catalogrecord", requested_pk=1 + ) + self.crid = self.cr_from_test_data["pk"] + self.permissionid = self.cr_from_test_data["fields"]["editor_permissions_id"] + self.editor_user_permission = self._get_whole_object_from_test_data( + "editoruserpermission", requested_pk=str(uuid.UUID(int=1)) + ) + self.metadata_provider_user = self.cr_from_test_data["fields"]["metadata_provider_user"] + self.userid = self.editor_user_permission["fields"]["user_id"] + self._use_http_authorization() + + def _get_whole_object_from_test_data(self, model_name, requested_pk=0): + + with open(test_data_file_path) as test_data_file: + test_data = json_load(test_data_file) + + model = "metax_api.%s" % model_name + + for row in test_data: + if row["model"] == model: + if row["pk"] == requested_pk: + obj = { + "id": row["pk"], + } + obj.update(row) + return obj + + raise Exception( + "Could not find model %s from test data with pk == %d. " + "Are you certain you generated rows for model %s in generate_test_data.py?" + % (model_name, requested_pk, model_name) + ) + + +class EditorUserPermissionApiWriteBasicTests(EditorUserPermissionApiWriteCommon): + + """ + Basic read operations + """ + + def test_write_editor_permission(self): + self._set_http_authorization("service") + data = {"role": "editor", "user_id": "test_editor"} + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + user_count = len(response.data) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + self.assertEqual(str(response.data["editor_permissions"]), self.permissionid) + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), user_count + 1) + + def test_write_editor_permission_invalid_data(self): + self._set_http_authorization("service") + data = {"role": "editor"} + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + user_count = len(response.data) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), user_count) + + def test_write_editor_permission_existing_userid(self): + self._set_http_authorization("service") + data = {"role": "editor", "user_id": "double_editor"} + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + user_count = len(response.data) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), user_count + 1) + + def test_write_editor_permission_change_values(self): + self._set_http_authorization("service") + data = {"role": "creator", "user_id": "change_editor"} + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + new_data = {"role": "editor"} + response = self.client.patch( + "/rest/datasets/%d/editor_permissions/users/%s" + % (self.crid, response.data.get("user_id")), + new_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(response.data.get("role"), "editor") + + def test_write_editor_permission_remove_users(self): + self._set_http_authorization("service") + data = {"role": "creator", "user_id": "new_creator"} + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + response = self.client.delete( + "/rest/datasets/%d/editor_permissions/users/%s" % (self.crid, data.get("user_id")) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + for user in response.data: + if user.get("role") == "creator": + response = self.client.delete( + "/rest/datasets/%d/editor_permissions/users/%s" + % (self.crid, user.get("user_id")) + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + else: + response = self.client.delete( + "/rest/datasets/%d/editor_permissions/users/%s" + % (self.crid, user.get("user_id")) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + + def test_write_editor_permission_add_removed_user(self): + self._set_http_authorization("service") + data = {"role": "editor", "user_id": "new_editor"} + # add + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + # remove + response = self.client.delete( + "/rest/datasets/%d/editor_permissions/users/%s" % (self.crid, data.get("user_id")) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + removed_user = EditorUserPermission.objects_unfiltered.get( + user_id="new_editor", editor_permissions_id=self.permissionid + ) + self.assertEqual(removed_user.removed, True) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + self.assertEqual(response.data.get("removed"), False) + + @responses.activate + def test_write_editor_permission_not_dataset_creator(self): + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", token={"group_names": [], "CSCUserName": "not_dataset_creator"} + ) + data = {"role": "editor", "user_id": "test_editor"} + response = self.client.post( + f"/rest/datasets/{self.crid}/editor_permissions/users", data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + @responses.activate + def test_write_editor_permission_provider_user(self): + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", token={"group_names": [], "CSCUserName": self.metadata_provider_user} + ) + data = {"role": "editor", "user_id": "test_editor"} + response = self.client.post( + f"/rest/datasets/{self.crid}/editor_permissions/users", data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + def test_write_editor_permission_list_service_with_no_write_access(self): + cr = CatalogRecord.objects.get(pk=self.crid) + cr.data_catalog.catalog_record_services_edit = "" + cr.data_catalog.save() + self._set_http_authorization("service") + data = {"role": "editor", "user_id": "test_editor"} + response = self.client.post( + f"/rest/datasets/{self.crid}/editor_permissions/users", data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/tests/api/rest/base/views/schemas/read.py b/src/metax_api/tests/api/rest/base/views/schemas/read.py index 88122020..10c11583 100755 --- a/src/metax_api/tests/api/rest/base/views/schemas/read.py +++ b/src/metax_api/tests/api/rest/base/views/schemas/read.py @@ -21,7 +21,7 @@ def test_read_schemas_list_html(self): headers = {"HTTP_ACCEPT": "text/html"} response = self.client.get("/rest/schemas", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response._headers["content-type"][1].find("json") >= 0) + # self.assertTrue(response.headers["content-type"][1].find("json") >= 0) def test_read_schema_retrieve_existing(self): list_response = self.client.get("/rest/schemas") diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py index 710ecfef..398e661c 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py @@ -612,6 +612,22 @@ def test_create_and_merge_draft(self): ) self.assertEqual("next_draft" in response.data, False, "next_draft link should be gone") + def test_create_and_merge_draft_keeps_permissions(self): + """ + Ensure creating and merging drafts keeps the same EditorPermission object. + """ + cr = self._create_dataset() + original_editor_permissions_id = CatalogRecordV2.objects.get(id=cr['id']).editor_permissions_id + + draft_cr = self._create_draft(cr["id"]) + draft_editor_permissions_id = CatalogRecordV2.objects.get(id=draft_cr['id']).editor_permissions_id + self.assertEqual(draft_editor_permissions_id, original_editor_permissions_id) + + self._merge_draft_changes(draft_cr["id"]) + merged_editor_permissions_id = CatalogRecordV2.objects.get(id=cr['id']).editor_permissions_id + self.assertEqual(merged_editor_permissions_id, original_editor_permissions_id) + + def test_missing_issued_date_is_generated_when_draft_is_merged(self): """ Testing a case where user removes 'issued_date' from draft before merging diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/write.py b/src/metax_api/tests/api/rest/v2/views/datasets/write.py index 43f3d012..7f30506f 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/write.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/write.py @@ -13,6 +13,7 @@ from rest_framework.test import APITestCase from metax_api.models import CatalogRecordV2, DataCatalog +from metax_api.services import RabbitMQService from metax_api.tests.utils import TestClassUtils, test_data_file_path CR = CatalogRecordV2 @@ -654,4 +655,73 @@ def test_external_service_can_not_add_catalog_record_to_other_catalog(self): self.cr_test_data["research_dataset"]["preferred_identifier"] = "temp-pid" response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) \ No newline at end of file + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + +class CatalogRecordRabbitMQPublish(CatalogRecordApiWriteCommon): + """ + Testing if RabbitMQ messages are published to correct exchanges with correct routing keys + when creating, updating, or deleting a catalog record. + Uses a dummy RabbitMQService. + """ + + def setUp(self): + super().setUp() + + def _check_rabbitmq_queue(self, routing_key, publish_to_etsin, publish_to_ttv): + """ + Checks if a message with a given routing key exists in the correct exchange + in the dummy RabbitMQService queue + """ + messages_str = ''.join(str(message) for message in RabbitMQService.messages) + assert_str_etsin = f"'routing_key': '{routing_key}', 'exchange': 'datasets'" + assert_str_ttv = f"'routing_key': '{routing_key}', 'exchange': 'ttv-datasets'" + + self.assertEqual(assert_str_etsin in messages_str, publish_to_etsin) + self.assertEqual(assert_str_ttv in messages_str, publish_to_ttv) + + + + def test_rabbitmq_publish(self): + """ + Creates four different data catalogs and creates, updates, and deletes + catalog records in them. Checks that RabbitMQ messages are published + in correct exchanges with correct routing keys. + """ + param_list = [(True, True), (True, False), (False, True), (False, False)] + for publish_to_etsin, publish_to_ttv in param_list: + with self.subTest(): + + # Create the data catalog + dc = self._get_object_from_test_data("datacatalog", 4) + dc_id = f"urn:nbn:fi:att:data-catalog-att-{publish_to_etsin}-{publish_to_ttv}" + dc["catalog_json"]["identifier"] = dc_id + dc["publish_to_etsin"] = publish_to_etsin + dc["publish_to_ttv"] = publish_to_ttv + dc = self.client.post("/rest/v2/datacatalogs", dc, format="json").data + + # Create the catalog record + cr = self._get_new_full_test_att_cr_data() + cr["data_catalog"] = dc + + cr = self.client.post("/rest/v2/datasets", cr, format="json").data + self._check_rabbitmq_queue("create", publish_to_etsin, publish_to_ttv) + + # Empty the queue + RabbitMQService.messages = [] + + # Update the catalog record + cr["research_dataset"]["description"] = { + "en": "Updating the description" + } + + response = self.client.put(f"/rest/v2/datasets/{cr['id']}", cr, format="json") + self._check_rabbitmq_queue("update", publish_to_etsin, publish_to_ttv) + + RabbitMQService.messages = [] + + # Delete the catalog record + response = self.client.delete(f"/rest/v2/datasets/{cr['id']}") + self._check_rabbitmq_queue("delete", publish_to_etsin, publish_to_ttv) + + RabbitMQService.messages = [] + diff --git a/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py b/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py index ab033762..f2d1230c 100755 --- a/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py @@ -660,3 +660,98 @@ def test_fix_deprecated_nested_directories_3(self): self.assertTrue( "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_12.txt" not in rd_filepaths ) + +class FlushUserDataTests(CatalogRecordApiWriteCommon): + """ + Tests that datasets created by a certain user can be flushed. + """ + + def test_existing_users_datasets_with_no_files_are_flushed(self): + self._use_http_authorization("metax") + + # create new dataset for user "abcd-1234" + username = "abcd-1234" + self.cr_test_data["metadata_provider_user"] = username + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) + response = self.client.post( + "/rest/v2/datasets?draft=false", self.cr_test_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + # make sure the dataset exists now + cr = response.data + response = self.client.get("/rest/datasets/{}".format(cr["identifier"]), format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["metadata_provider_user"], username) + + # flush user data for user "abcd-1234" and check that this succeeds + response = self.client.post("/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) + + response = self.client.get("/rest/datasets/{}".format(cr["identifier"]), format="json") + self.assertEqual("not found" in response.json()["detail"].lower(), True) + + def test_existing_users_datasets_with_files_are_flushed(self): + self._use_http_authorization("metax") + + # create new dataset for user "abcde-12345" + username = "abcde-12345" + self.cr_test_data["metadata_provider_user"] = username + response = self.client.post( + "/rest/v2/datasets?draft=false", self.cr_test_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + # make sure the dataset exists now + cr = response.data + response = self.client.get("/rest/datasets/{}".format(cr["identifier"]), format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["metadata_provider_user"], username) + + # save file related to dataset to later make sure it still exists + response = self.client.get("/rest/datasets/{}/files".format(cr["identifier"]), format="json") + cr_file = response.data[0]["identifier"] + + # flush user data for user "abcde-12345" and check that this succeeds + response = self.client.post("/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) + + response = self.client.get("/rest/datasets/{}".format(cr["identifier"]), format="json") + self.assertEqual("not found" in response.json()["detail"].lower(), True) + + # check that the file still exists (flushing datasets should not result to files being deleted) + response = self.client.get("/rest/files/{}".format(cr_file), format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_incorrect_parameters(self): + self._use_http_authorization("metax") + + # non existing username should return HTTP 404 + username = "doesnt_exist_123" + response = self.client.post("/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) + + # no metadata provider user should return HTTP 400 + response = self.client.post("/rpc/datasets/flush_user_data") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # wrong request method (delete instead of post) + username = "abcde-12345" + self.cr_test_data["metadata_provider_user"] = username + response = self.client.post( + "/rest/v2/datasets?draft=false", self.cr_test_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + response = self.client.delete( + "/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username + ) + self.assertEqual(response.status_code, 501) + + # wrong user + self._use_http_authorization("api_auth_user") + response = self.client.post( + "/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/tests/api/rpc/base/views/file_rpc.py b/src/metax_api/tests/api/rpc/base/views/file_rpc.py index 1e22da54..a380cb79 100755 --- a/src/metax_api/tests/api/rpc/base/views/file_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/file_rpc.py @@ -50,7 +50,7 @@ def test_wrong_parameters(self): response = self.client.post( "/rpc/files/delete_project?project_identifier=research_project_112" ) - # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_known_project_identifier(self): response = self.client.post( @@ -78,3 +78,72 @@ def test_datasets_are_marked_deprecated(self): self.client.post("/rpc/files/delete_project?project_identifier=project_x") response = self.client.get("/rest/datasets/%s" % related_dataset.identifier) self.assertEqual(response.data["deprecated"], True) + + +class FlushProjectTests(FileRPCTests): + + """ + Checks that an entire project's files and directories can be deleted. + """ + + def test_wrong_parameters(self): + # correct user, no project identifier + self._use_http_authorization("metax") + response = self.client.post("/rpc/files/flush_project") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # nonexisting project identifier: + response = self.client.post("/rpc/files/flush_project?project_identifier=non_existing") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # wrong request method + response = self.client.delete( + "/rpc/files/flush_project?project_identifier=research_project_112" + ) + self.assertEqual(response.status_code, 501) + + # wrong user + self._use_http_authorization("api_auth_user") + response = self.client.post( + "/rpc/files/flush_project?project_identifier=research_project_112" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_known_project_identifier(self): + self._use_http_authorization("metax") + response = self.client.post( + "/rpc/files/flush_project?project_identifier=research_project_112" + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_files_are_deleted(self): + self._use_http_authorization("metax") + + # make sure project has files before deleting them + files_count_before = File.objects.filter(project_identifier="research_project_112").count() + self.assertNotEqual(files_count_before, 0) + + # delete all files for project + response = self.client.post( + "/rpc/files/flush_project?project_identifier=research_project_112" + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # make sure all files are now deleted + files_count_after = File.objects.filter(project_identifier="research_project_112").count() + self.assertEqual(files_count_after, 0) + + def test_directories_are_deleted(self): + self._use_http_authorization("metax") + + # make sure project has directories before deleting them + dirs_count_before = Directory.objects.filter(project_identifier="research_project_112").count() + self.assertNotEqual(dirs_count_before, 0) + + # delete all directories for project + response = self.client.post("/rpc/files/flush_project?project_identifier=research_project_112") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # make sure all directories are now deleted + dirs_count_after = Directory.objects.filter(project_identifier="research_project_112").count() + self.assertEqual(dirs_count_after, 0) \ No newline at end of file diff --git a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py index e330e325..5d57b244 100755 --- a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py @@ -932,3 +932,61 @@ def test_organization_datasets_cumulative_for_drafts(self): # ensure the totals are calculated without drafts self.assertNotEqual(total_1, total_2, "Count be reduced by one after setting id=1 as draft") + + +class StatisticRPCforOrganizationDatasetsCumulative(StatisticRPCCommon, CatalogRecordApiWriteCommon): + """ + Test suite for organization_datasets_cumulative. Test only optional parameters removed, legacy and latest for now. + """ + + url = "/rpc/statistics/organization_datasets_cumulative" + dateparam_all = "from_date=2018-06-01&to_date=2018-06-30" + + def test_organization_datasets_cumulative_removed(self): + # initially there are 2 datasets + response = self.client.get(f"{self.url}?{self.dateparam_all}").data + self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 2) + + # removed=true should return 0 datasets since none have been removed + response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true").data + self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 0) + + # remove one dataset from June, so one dataset should be left + self._set_removed_dataset(id=8) + response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=false").data + self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 1) + + # removed=true should return 1 removed dataset now + response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true").data + self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 1) + + def test_organization_datasets_cumulative_legacy(self): + # let's create 2 legacy datasets for 2018-06 + leg_cr = ( + self._create_legacy_dataset() + ) + self._set_dataset_creation_date(leg_cr, "2018-06-13") + leg_cr2 = ( + self._create_legacy_dataset() + ) + self._set_dataset_creation_date(leg_cr2, "2018-06-13") + + response = self.client.get(f"{self.url}?{self.dateparam_all}&legacy=true").data + self.assertEqual(response["some_org_id"]["urn:nbn:fi:att:data-catalog-legacy"][0]["count"], 2) + + # check that legacy=false returns 0 datasets + response = self.client.get(f"{self.url}?{self.dateparam_all}&legacy=false").data + self.assertEqual(response["some_org_id"]["urn:nbn:fi:att:data-catalog-legacy"][0]["count"], 0) + + def test_organization_datasets_cumulative_latest(self): + # let's create 2 versions of a dataset for 2018-06 + first_version = self._create_new_dataset_version() + self._set_dataset_creation_date(first_version, "2018-06-13") + second_version = self._create_new_dataset_version() + self._set_dataset_creation_date(second_version, "2018-06-13") + response = self.client.get(f"{self.url}?{self.dateparam_all}&latest=false").data # returns all + self.assertEqual(response["org_1"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 2) + + # latest=true should only return the latest version + response = self.client.get(f"{self.url}?{self.dateparam_all}&latest=true").data + self.assertEqual(response["org_1"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 1) \ No newline at end of file diff --git a/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py b/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py index d89f0c29..d61fce67 100755 --- a/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py +++ b/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py @@ -121,6 +121,21 @@ def test_create_new_version(self): response.data["identifier"], ) + def test_create_new_version_shares_permissions(self): + """ + Ensure new version shares EditorPermissions with the original. + """ + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=5", format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + next_version_identifier = response.data.get("identifier") + cr = CR.objects.get(id=5) + next_version_cr = CR.objects.get(identifier=next_version_identifier) + self.assertEqual(cr.editor_permissions_id, next_version_cr.editor_permissions_id) + + def test_delete_new_version_draft(self): """ Ensure a new version that is created into draft state can be deleted, and is permanently deleted. diff --git a/src/metax_api/tests/testdata/data_catalog_test_data_template.json b/src/metax_api/tests/testdata/data_catalog_test_data_template.json index decbf52a..34cc8604 100755 --- a/src/metax_api/tests/testdata/data_catalog_test_data_template.json +++ b/src/metax_api/tests/testdata/data_catalog_test_data_template.json @@ -117,7 +117,7 @@ "dataset_versioning": true }, "service_created": "metax", - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax" + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas" } \ No newline at end of file diff --git a/src/metax_api/tests/testdata/generate_test_data.py b/src/metax_api/tests/testdata/generate_test_data.py index 6729396a..85dcaf78 100755 --- a/src/metax_api/tests/testdata/generate_test_data.py +++ b/src/metax_api/tests/testdata/generate_test_data.py @@ -7,6 +7,7 @@ import os import sys +import uuid from copy import deepcopy from json import dump as json_dump, load as json_load @@ -268,6 +269,7 @@ def save_test_data( contract_list, catalog_record_list, dataset_version_sets, + editor_permissions, ): with open("test_data.json", "w") as f: print("dumping test data as json to metax_api/tests/test_data.json...") @@ -278,6 +280,7 @@ def save_test_data( + data_catalogs_list + contract_list + dataset_version_sets + + editor_permissions + catalog_record_list, f, indent=4, @@ -368,6 +371,30 @@ def generate_contracts(contract_max_rows, validate_json): return test_contract_list +def add_editor_permissions(editor_permissions, dataset): + # add EditorPermissions + pk = len(editor_permissions) + rights_pk = str(uuid.UUID(int=pk)) + editor_perms = { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": rights_pk, + } + editor_permissions.append(editor_perms) + editor_user_perms = { + "fields": { + "user_id": dataset["fields"]["metadata_provider_user"], + "date_created": dataset["fields"]["date_created"], + "editor_permissions_id": rights_pk, + "role": "creator", + }, + "model": "metax_api.editoruserpermission", + "pk": str(uuid.UUID(int=pk + 1)), + } + editor_permissions.append(editor_user_perms) + dataset["fields"]["editor_permissions_id"] = rights_pk + + def generate_catalog_records( basic_catalog_record_max_rows, data_catalogs_list, @@ -377,6 +404,7 @@ def generate_catalog_records( type, test_data_list=[], dataset_version_sets=[], + editor_permissions=[], ): print("generating %s catalog records..." % type) @@ -426,6 +454,8 @@ def generate_catalog_records( new["fields"]["date_created"] = "2017-05-23T10:07:22Z" new["fields"]["files"] = [] + add_editor_permissions(editor_permissions, new) + # add files if type == "ida": @@ -454,62 +484,46 @@ def generate_catalog_records( # first fifth of files dataset_files[-1]["file_type"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", - "pref_label": { - "fi": "Teksti", - "en": "Text", - "und": "Teksti" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" + "pref_label": {"fi": "Teksti", "en": "Text", "und": "Teksti"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", } dataset_files[-1]["use_category"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", "pref_label": { "fi": "Lähdeaineisto", "en": "Source material", - "und": "Lähdeaineisto" + "und": "Lähdeaineisto", }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", } elif file_divider <= j < (file_divider * 2): # second fifth of files dataset_files[-1]["file_type"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video", - "pref_label": { - "fi": "Video", - "en": "Video", - "und": "Video" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" + "pref_label": {"fi": "Video", "en": "Video", "und": "Video"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", } dataset_files[-1]["use_category"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome", "pref_label": { "fi": "Tulosaineisto", "en": "Outcome material", - "und": "Tulosaineisto" + "und": "Tulosaineisto", }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", } elif (file_divider * 2) <= j < (file_divider * 3): # third fifth of files dataset_files[-1]["file_type"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image", - "pref_label": { - "fi": "Kuva", - "en": "Image", - "und": "Kuva" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" + "pref_label": {"fi": "Kuva", "en": "Image", "und": "Kuva"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", } dataset_files[-1]["use_category"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication", - "pref_label": { - "fi": "Julkaisu", - "en": "Publication", - "und": "Julkaisu" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" + "pref_label": {"fi": "Julkaisu", "en": "Publication", "und": "Julkaisu"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", } elif (file_divider * 3) <= j < (file_divider * 4): # fourth fifth of files @@ -518,18 +532,18 @@ def generate_catalog_records( "pref_label": { "fi": "Lähdekoodi", "en": "Source code", - "und": "Lähdekoodi" + "und": "Lähdekoodi", }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", } dataset_files[-1]["use_category"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation", "pref_label": { "fi": "Dokumentaatio", "en": "Documentation", - "und": "Dokumentaatio" + "und": "Dokumentaatio", }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", } else: # the rest of files @@ -652,6 +666,8 @@ def generate_catalog_records( new["fields"]["date_modified"] = "2017-09-23T10:07:22Z" new["fields"]["date_created"] = "2017-05-23T10:07:22Z" + add_editor_permissions(editor_permissions, new) + new["fields"]["research_dataset"]["metadata_version_identifier"] = generate_test_identifier( cr_type, len(test_data_list) + 1, urn=False ) @@ -761,7 +777,7 @@ def generate_catalog_records( json_validate(new["fields"]["research_dataset"], json_schema) test_data_list.append(new) - return test_data_list, dataset_version_sets + return test_data_list, dataset_version_sets, editor_permissions def generate_alt_catalog_records(test_data_list): @@ -844,7 +860,7 @@ def set_qvain_info_to_records(catalog_record_list): ida_data_catalog_max_rows + 1, att_data_catalog_max_rows, validate_json, "att" ) - catalog_record_list, dataset_version_sets = generate_catalog_records( + catalog_record_list, dataset_version_sets, editor_permissions = generate_catalog_records( ida_catalog_record_max_rows, ida_data_catalogs_list, contract_list, @@ -853,7 +869,7 @@ def set_qvain_info_to_records(catalog_record_list): "ida", ) - catalog_record_list, dataset_version_sets = generate_catalog_records( + catalog_record_list, dataset_version_sets, editor_permissions = generate_catalog_records( att_catalog_record_max_rows, att_data_catalogs_list, contract_list, @@ -862,6 +878,7 @@ def set_qvain_info_to_records(catalog_record_list): "att", catalog_record_list, dataset_version_sets, + editor_permissions, ) catalog_record_list = generate_alt_catalog_records(catalog_record_list) @@ -876,6 +893,7 @@ def set_qvain_info_to_records(catalog_record_list): contract_list, catalog_record_list, dataset_version_sets, + editor_permissions, ) print("done") diff --git a/src/metax_api/tests/testdata/test_data.json b/src/metax_api/tests/testdata/test_data.json index c3802a10..025b175e 100755 --- a/src/metax_api/tests/testdata/test_data.json +++ b/src/metax_api/tests/testdata/test_data.json @@ -4537,9 +4537,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -4667,9 +4667,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -4797,9 +4797,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -4927,9 +4927,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5057,9 +5057,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5187,9 +5187,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5317,9 +5317,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5447,9 +5447,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5712,6 +5712,396 @@ "model": "metax_api.datasetversionset", "pk": 13 }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000000" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000000", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000001" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000002" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000002", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000003" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000004" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000004", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000005" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000006" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000006", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000007" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000008" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000008", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000009" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000000a" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000a", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000000b" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000000c" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000c", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000000d" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000000e" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000e", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000000f" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000010" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000010", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000011" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000012" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000012", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000013" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000014" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000014", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000015" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000016" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000016", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000017" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000018" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000018", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000019" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000001a" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001a", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000001b" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000001c" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001c", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000001d" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000001e" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001e", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000001f" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000020" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000020", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000021" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000022" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000022", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000023" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000024" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000024", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000025" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000026" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000026", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000027" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000028" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000028", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000029" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000002a" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002a", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000002b" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000002c" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002c", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000002d" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000002e" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002e", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000002f" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000030" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000030", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000031" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000032" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000032", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000033" + }, { "fields": {}, "model": "metax_api.alternaterecordset", @@ -5727,6 +6117,7 @@ "dataset_version_set": 1, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000000", "files": [ 1, 2 @@ -5882,6 +6273,7 @@ "dataset_version_set": 2, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000002", "files": [ 3, 4 @@ -6037,6 +6429,7 @@ "dataset_version_set": 3, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000004", "files": [ 5, 6 @@ -6192,6 +6585,7 @@ "dataset_version_set": 4, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000006", "files": [ 7, 8 @@ -6347,6 +6741,7 @@ "dataset_version_set": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000008", "files": [ 9, 10 @@ -6502,6 +6897,7 @@ "dataset_version_set": 6, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000a", "files": [ 11, 12 @@ -6657,6 +7053,7 @@ "dataset_version_set": 7, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000c", "files": [ 13, 14 @@ -6812,6 +7209,7 @@ "dataset_version_set": 8, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000e", "files": [ 15, 16 @@ -6952,6 +7350,7 @@ "dataset_version_set": 9, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000010", "files": [ 17, 18 @@ -7078,6 +7477,7 @@ "dataset_version_set": 10, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000012", "files": [ 19, 20 @@ -7201,6 +7601,7 @@ "dataset_version_set": 11, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000014", "files": [ 1, 2, @@ -8152,6 +8553,7 @@ "dataset_version_set": 12, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000016", "files": [ 1, 2, @@ -9103,6 +9505,7 @@ "dataset_version_set": 13, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000018", "files": [ 22, 23, @@ -10146,6 +10549,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001a", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9614", "metadata_owner_org": "abc-org-123", @@ -10285,6 +10689,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001c", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9615", "metadata_owner_org": "abc-org-123", @@ -10424,6 +10829,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001e", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9616", "metadata_owner_org": "abc-org-123", @@ -10563,6 +10969,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000020", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9617", "metadata_owner_org": "abc-org-123", @@ -10702,6 +11109,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000022", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9618", "metadata_owner_org": "abc-org-123", @@ -10841,6 +11249,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000024", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9619", "metadata_owner_org": "abc-org-123", @@ -10980,6 +11389,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000026", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9620", "metadata_owner_org": "abc-org-123", @@ -11110,6 +11520,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000028", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9621", "metadata_owner_org": "abc-org-123", @@ -11240,6 +11651,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002a", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9622", "metadata_owner_org": "abc-org-123", @@ -11370,6 +11782,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002c", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9623", "metadata_owner_org": "abc-org-123", @@ -11499,6 +11912,7 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002e", "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9624", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -12433,6 +12847,7 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000030", "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9625", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -13367,6 +13782,7 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000032", "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9626", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -14304,6 +14720,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000012", "files": [ 19, 20 @@ -14429,6 +14846,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000012", "files": [ 19, 20