From 399eb7796c5149358304b51a2e4123a559e59921 Mon Sep 17 00:00:00 2001
From: mboudet
Date: Fri, 1 Jul 2022 20:17:02 +0200
Subject: [PATCH] Release 4.4.0 (#356)
---
.github/workflows/lint_test.yml | 6 +-
CHANGELOG.md | 54 +
Makefile | 39 +-
Pipfile | 4 +-
Pipfile.lock | 1295 ++++++++++-------
README.md | 1 +
askomics/api/admin.py | 310 ++++
askomics/api/data.py | 12 +-
askomics/api/datasets.py | 7 +
askomics/api/file.py | 31 +-
askomics/api/ontology.py | 59 +
askomics/api/query.py | 8 +-
askomics/api/results.py | 10 +-
askomics/api/sparql.py | 2 +-
askomics/api/start.py | 9 +-
askomics/app.py | 4 +-
askomics/libaskomics/BedFile.py | 32 +-
askomics/libaskomics/CsvFile.py | 114 +-
askomics/libaskomics/Database.py | 115 ++
askomics/libaskomics/Dataset.py | 61 +-
askomics/libaskomics/DatasetsHandler.py | 10 +-
askomics/libaskomics/File.py | 12 +-
askomics/libaskomics/FilesHandler.py | 122 +-
askomics/libaskomics/GffFile.py | 56 +-
askomics/libaskomics/LocalAuth.py | 20 +-
askomics/libaskomics/OntologyManager.py | 279 ++++
askomics/libaskomics/PrefixManager.py | 82 +-
askomics/libaskomics/RdfFile.py | 33 +-
askomics/libaskomics/RdfGraph.py | 1 +
askomics/libaskomics/Result.py | 29 +-
askomics/libaskomics/SparqlQuery.py | 254 +++-
askomics/libaskomics/SparqlQueryLauncher.py | 15 +-
askomics/libaskomics/TriplestoreExplorer.py | 69 +-
.../react/src/components/autocomplete.jsx | 147 ++
askomics/react/src/navbar.jsx | 4 +-
askomics/react/src/routes.jsx | 9 +-
askomics/react/src/routes/admin/admin.jsx | 3 +-
.../react/src/routes/admin/datasetstable.jsx | 2 +-
.../react/src/routes/admin/ontologies.jsx | 365 +++++
askomics/react/src/routes/admin/prefixes.jsx | 257 ++++
askomics/react/src/routes/ask/ask.jsx | 7 +-
askomics/react/src/routes/data/data.jsx | 10 +-
.../src/routes/datasets/datasetstable.jsx | 4 +-
askomics/react/src/routes/form/attribute.jsx | 100 +-
askomics/react/src/routes/form/query.jsx | 17 +-
.../react/src/routes/form_edit/attribute.jsx | 100 +-
askomics/react/src/routes/form_edit/query.jsx | 20 +-
.../routes/integration/advancedoptions.jsx | 10 +-
.../src/routes/integration/bedpreview.jsx | 23 +-
.../react/src/routes/integration/csvtable.jsx | 74 +-
.../src/routes/integration/gffpreview.jsx | 30 +-
.../src/routes/integration/integration.jsx | 5 +-
.../src/routes/integration/rdfpreview.jsx | 23 +-
askomics/react/src/routes/query/attribute.jsx | 142 +-
.../react/src/routes/query/graphfilters.js | 8 +-
.../react/src/routes/query/ontolinkview.jsx | 46 +
askomics/react/src/routes/query/query.jsx | 163 ++-
.../react/src/routes/query/visualization.jsx | 2 +-
.../src/routes/results/resultsfilestable.jsx | 3 +-
.../react/src/routes/sparql/resultstable.jsx | 2 +-
.../react/src/routes/upload/filestable.jsx | 15 +
.../react/src/routes/upload/uploadurlform.jsx | 6 +-
askomics/static/about.html | 2 +-
askomics/static/css/askomics.css | 35 +-
askomics/tasks.py | 41 +-
config/askomics.ini.template | 36 +-
config/askomics.test.ini | 33 +-
docker/Dockerfile | 6 +-
docker/DockerfileAll | 6 +-
docker/DockerfileCelery | 4 +-
docs/abstraction.md | 179 ++-
docs/ci.md | 2 +-
docs/cli.md | 13 +
docs/configure.md | 5 +-
docs/console.md | 51 +
docs/contribute.md | 6 +-
docs/data.md | 181 ++-
docs/dev-deployment.md | 6 +-
docs/docs.md | 8 +-
docs/federation.md | 36 +-
docs/galaxy.md | 19 +-
docs/img/askogalaxy.png | Bin 15352 -> 15318 bytes
docs/img/askograph.png | Bin 0 -> 34589 bytes
docs/img/attribute_box.png | Bin 0 -> 1560 bytes
docs/img/attributes.png | Bin 0 -> 15032 bytes
docs/img/csv_convert.png | Bin 0 -> 251893 bytes
docs/img/custom_nodes.png | Bin 0 -> 5745 bytes
docs/img/external_startpoint.png | Bin 11895 -> 11646 bytes
docs/img/faldo.png | Bin 0 -> 18666 bytes
docs/img/filters.png | Bin 0 -> 2612 bytes
docs/img/form.png | Bin 0 -> 4052 bytes
docs/img/form_edit.png | Bin 0 -> 12392 bytes
docs/img/form_example.png | Bin 0 -> 12575 bytes
docs/img/gff.png | Bin 2854 -> 2275 bytes
docs/img/gff_preview.png | Bin 11601 -> 11892 bytes
docs/img/minus.png | Bin 0 -> 13218 bytes
docs/img/ontology_autocomplete.png | Bin 0 -> 13559 bytes
docs/img/ontology_graph.png | Bin 0 -> 17176 bytes
docs/img/ontology_integration.png | Bin 0 -> 6323 bytes
docs/img/ontology_link.png | Bin 0 -> 13408 bytes
docs/img/sparql.png | Bin 0 -> 119634 bytes
docs/img/startpoint.png | Bin 15514 -> 15434 bytes
docs/img/template.png | Bin 0 -> 3700 bytes
docs/img/union.png | Bin 0 -> 16347 bytes
docs/img/union_duplicated.png | Bin 0 -> 15609 bytes
docs/index.md | 18 +-
docs/issues.md | 1 -
docs/manage.md | 45 +-
docs/ontologies.md | 60 +
docs/prefixes.md | 11 +
docs/production-deployment.md | 8 +-
docs/query.md | 299 ++++
docs/requirements.txt | 3 +-
docs/results.md | 79 +
docs/style.css | 23 +-
docs/template.md | 98 ++
docs/tutorial.md | 115 +-
mkdocs.yml | 18 +-
package-lock.json | 160 +-
package.json | 8 +-
setup.py | 6 +-
test-data/agro_min.ttl | 70 +
test-data/gene.bed | 2 +-
test-data/gene.gff3 | 2 +
test-data/transcripts.tsv | 22 +-
tests/__init__.py | 3 +
tests/conftest.py | 95 +-
tests/results/abstraction.json | 62 +-
tests/results/data.json | 54 -
tests/results/data_full.json | 62 +
tests/results/data_public.json | 54 +
tests/results/init.json | 2 +-
tests/results/preview.json | 28 +-
tests/results/preview_files.json | 36 +-
tests/results/preview_malformed_files.json | 2 +-
tests/results/results.json | 2 +-
tests/results/results_admin.json | 2 +-
tests/results/results_form.json | 2 +-
tests/results/sparql_and_graph.json | 3 +-
tests/results/sparql_preview.json | 22 +-
tests/results/sparql_query.json | 2 +-
tests/results/startpoints.json | 12 +-
tests/test_api.py | 5 +-
tests/test_api_admin.py | 247 +++-
tests/test_api_data.py | 12 +-
tests/test_api_datasets.py | 30 +-
tests/test_api_file.py | 95 +-
tests/test_api_ontology.py | 73 +
tests/test_api_query.py | 6 +-
149 files changed, 5825 insertions(+), 1555 deletions(-)
create mode 100644 askomics/api/ontology.py
create mode 100644 askomics/libaskomics/OntologyManager.py
create mode 100644 askomics/react/src/components/autocomplete.jsx
create mode 100644 askomics/react/src/routes/admin/ontologies.jsx
create mode 100644 askomics/react/src/routes/admin/prefixes.jsx
create mode 100644 askomics/react/src/routes/query/ontolinkview.jsx
create mode 100644 docs/cli.md
create mode 100644 docs/console.md
create mode 100644 docs/img/askograph.png
create mode 100644 docs/img/attribute_box.png
create mode 100644 docs/img/attributes.png
create mode 100644 docs/img/csv_convert.png
create mode 100644 docs/img/custom_nodes.png
create mode 100644 docs/img/faldo.png
create mode 100644 docs/img/filters.png
create mode 100644 docs/img/form.png
create mode 100644 docs/img/form_edit.png
create mode 100644 docs/img/form_example.png
create mode 100644 docs/img/minus.png
create mode 100644 docs/img/ontology_autocomplete.png
create mode 100644 docs/img/ontology_graph.png
create mode 100644 docs/img/ontology_integration.png
create mode 100644 docs/img/ontology_link.png
create mode 100644 docs/img/sparql.png
create mode 100644 docs/img/template.png
create mode 100644 docs/img/union.png
create mode 100644 docs/img/union_duplicated.png
delete mode 100644 docs/issues.md
create mode 100644 docs/ontologies.md
create mode 100644 docs/prefixes.md
create mode 100644 docs/query.md
create mode 100644 docs/results.md
create mode 100644 docs/template.md
create mode 100644 test-data/agro_min.ttl
delete mode 100644 tests/results/data.json
create mode 100644 tests/results/data_full.json
create mode 100644 tests/results/data_public.json
create mode 100644 tests/test_api_ontology.py
diff --git a/.github/workflows/lint_test.yml b/.github/workflows/lint_test.yml
index 1be124fb..84ca6d05 100644
--- a/.github/workflows/lint_test.yml
+++ b/.github/workflows/lint_test.yml
@@ -8,7 +8,7 @@ jobs:
uses: actions/checkout@v2
- uses: actions/setup-python@v1
with:
- python-version: 3.7
+ python-version: 3.8
- name: Install flake8
run: pip install flake8
- name: Flake8
@@ -33,7 +33,7 @@ jobs:
uses: actions/checkout@v2
- uses: actions/setup-python@v1
with:
- python-version: 3.6
+ python-version: 3.8
- name: Update apt cache
run: sudo apt-get update
- name: Install python-ldap deps
@@ -50,7 +50,7 @@ jobs:
docker pull xgaia/corese:latest
docker pull xgaia/isql-api:2.1.1
docker pull xgaia/simple-ldap:latest
- docker run -d --name virtuoso -p 8891:8890 -p 1112:1111 -e DBA_PASSWORD=dba -e SPARQL_UPDATE=true -e DEFAULT_GRAPH=http://localhost:8891/DAV -t askomics/virtuoso:7.2.5.1 /bin/sh -c "netstat -nr | grep '^0\.0\.0\.0' | grep -oE '((1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\.){3}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])' | grep -v '^0\.0\.0\.0' | sed 's/$/ askomics-host/' >> /etc/hosts && /virtuoso/virtuoso.sh"
+ docker run -d --name virtuoso -p 8891:8890 -p 1112:1111 -e DBA_PASSWORD=dba -e DEFAULT_GRAPH=http://localhost:8891/DAV -t askomics/virtuoso:7.2.5.1 /bin/sh -c "netstat -nr | grep '^0\.0\.0\.0' | grep -oE '((1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\.){3}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])' | grep -v '^0\.0\.0\.0' | sed 's/$/ askomics-host/' >> /etc/hosts && /virtuoso/virtuoso.sh"
sleep 1m
docker run -d --name redis -p 6380:6379 -t redis:4.0
docker run -d --name galaxy -p 8081:80 -t bgruening/galaxy-stable:20.05
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6897dd9..0766ab02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
This changelog was started for release 4.2.0.
+## [4.4.0] - 2022-07-01
+
+### Fixed
+
+- Fixed an issue with forms (missing field and entity name for label & uri fields) (Issue #255)
+- Fixed an issue with the data endpoint for FALDO entities (Issue #279)
+- Fixed an issue where integration would fail when setting a category type on a empty column (#334)
+- Fixed an issue with saved queries for non-logged users
+
+### Added
+
+- Added 'scaff' for autodetection of 'reference' columns
+- Added a 'Label' column type: only for second column in CSV files. Will use this value if present, else default to old behavior
+- Added button to hide FALDO relations (*included_in*)
+- Added 'target=_blank' in query results
+- Remote upload is now sent in a Celery task
+- Added 'Status' for files (for celery upload, and later for better file management)
+- Added tooltips to buttons in the query form (and other forms)
+- Added owl integration
+- Add better error management for RDF files
+- Added 'single tenant' mode: Send queries to all graphs to speed up
+- Added ontologies management
+- Added prefixes management
+- Added 'external graph' management for federated request: federated requests will only target this remote graph
+- Added support for multithread in web server, with the *WORKERS* env variable when calling make
+
+### Changed
+
+- Changed "Query builder" to "Form editor" in form editing interface
+- Changed abstraction building method for relations. (Please refer to #248 and #268). Correct 'phantom' relations
+- Changed abstraction building method for attributes. (Please refer to #321 and #324). Correct 'attributes' relations
+- Changed abstraction building method for 'strand': only add the required strand type, and not all three types (#277)
+- Updated documentation
+- Changed the sparql endpoint: now use the authenticated SPARQL endpoint instead of public endpoint. Write permissions are not required anymore
+- Reverted base docker image to alpine-13 to solve a docker issue
+
+### Removed
+
+- Removed "Remote endpoint" field for non-ttl file
+- Removed "Custom_uri" field for ttl file
+
+### Security
+
+- Bump axios from 0.21.1 to 0.21.2
+- Bump tar from 6.1.0 to 6.1.11
+- Bump @npmcli/git from 2.0.6 to 2.1.0
+- Bump path-parse from 1.0.6 to 1.0.7
+- Bump prismjs from 1.23.0 to 1.27.0
+- Bump simple-get from 2.8.1 to 2.8.2
+- Bump ssri from 6.0.1 to 6.0.2
+- Bump follow-redirects from 1.14.4 to 1.14.8
+- Bump mkdocs from 1.0.4 to 1.2.3 in /docs
+- Bump python-ldap from 3.3.1 to 3.4.0
+- Bump minimist from 1.2.5 to 1.2.6
## [4.3.1] - 2021-06-16
diff --git a/Makefile b/Makefile
index 7b1a35bf..12f74d0c 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,7 @@ FLASKOPTS=
PYTESTOPTS=
TESTFILE?=tests
NTASKS?=1
+WORKERS?=1
HOST?=0.0.0.0
PORT?=5000
@@ -52,20 +53,20 @@ help:
@echo ' make clean Uninstall everything'
@echo ' make install [MODE=dev] Install Python and Js dependencies (+ dev dependencies if MODE=dev)'
@echo ' make build [MODE=dev] Build javascript (and watch for update if MODE=DEV)'
- @echo ' make serve [MODE=dev] [HOST=0.0.0.0] [PORT=5000] [NTASKS=1] Serve AskOmics at $(HOST):$(PORT)'
+ @echo ' make serve [MODE=dev] [HOST=0.0.0.0] [PORT=5000] [NTASKS=1] [WORKERS=1] Serve AskOmics at $(HOST):$(PORT)'
@echo ' make test Lint and test javascript and python code'
@echo ' make serve-doc [DOCPORT=8000] Serve documentation at localhost:$(DOCPORT)'
@echo ' make update-base-url Update all graphs from an old base_url to a new base_url'
@echo ' make clear-cache Clear abstraction cache'
@echo ''
@echo 'Examples:'
- @echo ' make clean install build serve NTASKS=10 Clean install and serve AskOmics in production mode, 10 celery tasks in parallel'
- @echo ' make clean install serve MODE=dev NTASKS=10 Clean install and serve AskOmics in development mode, 10 celery tasks in parallel'
+ @echo ' make clean install build serve NTASKS=10 WORKERS=2 Clean install and serve AskOmics in production mode, 10 celery tasks in parallel, 2 workers on the web server'
+ @echo ' make clean install serve MODE=dev NTASKS=10 WORKERS=2 Clean install and serve AskOmics in development mode, 10 celery tasks in parallel, 2 workers on the web server'
@echo ''
@echo ' make clean install Clean install AskOmics'
@echo ' make clean install MODE=dev Clean install AskOmics in development mode'
- @echo ' make serve NTASKS=10 Serve AskOmics, 10 celery tasks in parallel'
- @echo ' make serve MODE=dev NTASKS=10 Serve AskOmics in development mode, 10 celery tasks in parallel'
+ @echo ' make serve NTASKS=10 WORKERS=2 Serve AskOmics, 10 celery tasks in parallel, 2 workers on the web server'
+ @echo ' make serve MODE=dev NTASKS=10 WORKERS=2 Serve AskOmics in development mode, 10 celery tasks in parallel, 2 workers on the web server'
@echo ''
@echo ' make pytest MODE=dev TESTFILE=tests/test_api.py Test tests/test_api file only'
@@ -81,7 +82,7 @@ test-js: eslint
eslint: check-node-modules
@echo -n 'Linting javascript... '
- $(NODEDIR)/.bin/eslint --config $(BASEDIR)/.eslintrc.yml "$(BASEDIR)/askomics/react/src/**"
+ $(NODEDIR)/.bin/eslint --config $(BASEDIR)/.eslintrc.yml "$(BASEDIR)/askomics/react/src/**" || { echo "ERROR"; exit 1; }
@echo "Done"
test-python: pylint pytest
@@ -94,7 +95,7 @@ pytest: check-venv
pylint: check-venv
@echo -n 'Linting python... '
. $(ACTIVATE)
- flake8 $(BASEDIR)/askomics $(BASEDIR)/tests --ignore=E501,W504
+ flake8 $(BASEDIR)/askomics $(BASEDIR)/tests --ignore=E501,W504 || { echo "ERROR"; exit 1; }
@echo "Done"
serve: check-venv build-config create-user
@@ -106,7 +107,7 @@ serve-askomics: check-venv build-config create-user
ifeq ($(MODE), dev)
FLASK_ENV=development FLASK_APP=app flask run --host=$(HOST) --port $(PORT)
else
- FLASK_ENV=production FLASK_APP=app gunicorn -b $(HOST):$(PORT) app
+ FLASK_ENV=production FLASK_APP=app gunicorn -w $(WORKERS) -b $(HOST):$(PORT) app
endif
serve-celery: check-venv build-config create-user
@@ -126,32 +127,32 @@ check-node-modules:
build-config:
@echo -n 'Building config file... '
- bash cli/set_config.sh
+ bash cli/set_config.sh || { echo "ERROR"; exit 1; }
@echo 'Done'
create-user:
@echo -n 'Creating first user... '
. $(ACTIVATE)
- bash cli/set_user.sh
+ bash cli/set_user.sh || { echo "ERROR"; exit 1; }
@echo 'Done'
update-base-url: check-venv
@echo 'Updating base url...'
. $(ACTIVATE)
- bash cli/update_base_url.sh
+ bash cli/update_base_url.sh || { echo "ERROR"; exit 1; }
@echo 'Done'
clear-cache: check-venv
@echo 'Clearing abstraction cache...'
. $(ACTIVATE)
- bash cli/clear_cache.sh
+ bash cli/clear_cache.sh || { echo "ERROR"; exit 1; }
@echo 'Done'
build: build-js
build-js: check-node-modules
@echo 'Building askomics.js... '
- $(NPM) run --silent $(NPMOPTS)
+ $(NPM) run --silent $(NPMOPTS) || { echo "ERROR"; exit 1; }
@echo ' Done'
install: install-python install-js
@@ -161,22 +162,22 @@ fast-install:
install-python: check-python
@echo -n 'Building python virtual environment... '
- $(PYTHON) -m venv $(VENVDIR)
+ $(PYTHON) -m venv $(VENVDIR) || { echo "ERROR"; exit 1; }
@echo 'Done'
@echo -n 'Sourcing Python virtual environment... '
- . $(ACTIVATE)
+ . $(ACTIVATE) || { echo "ERROR"; exit 1; }
@echo 'Done'
@echo -n 'Upgrading pip... '
- $(PIP) install --upgrade pip > /dev/null
+ $(PIP) install --upgrade pip > /dev/null || { echo "ERROR"; exit 1; }
@echo 'Done'
@echo 'Installing Python dependencies inside virtual environment... '
- $(PIP) install -e . > /dev/null
- PIPENV_VERBOSITY=-1 $(PIPENV) install $(PIPENVOPTS)
+ $(PIP) install -e . > /dev/null || { echo "ERROR"; exit 1; }
+ PIPENV_VERBOSITY=-1 $(PIPENV) install $(PIPENVOPTS) || { echo "ERROR"; exit 1; }
@echo ' Done'
install-js: check-npm
@echo 'Installing javascript dependencies inside node_modules... '
- $(NPM) install --silent
+ $(NPM) install || { echo "ERROR"; exit 1; }
@echo ' Done'
clean: clean-js clean-python
diff --git a/Pipfile b/Pipfile
index 0dde7cdf..6509b8eb 100644
--- a/Pipfile
+++ b/Pipfile
@@ -5,7 +5,7 @@ name = "pypi"
[packages]
werkzeug = "==0.16.1"
-flask = "==1.1.4"
+flask = "<2"
flask-reverse-proxy-fix = "*"
validate-email = "*"
gunicorn = "*"
@@ -13,7 +13,7 @@ python-magic = "*"
rdflib = "*"
sparqlwrapper = "*"
requests = "*"
-celery = "*"
+celery = "==5.0.5"
redis = "*"
watchdog = "*"
gitpython = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 6c8940c3..ef4d555b 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "f208e36a35ade8b9d94b025ec7085c79f6b57dbd69ae9dd2b08c8b06418242fe"
+ "sha256": "5d768dd0f5c397f0380f7a594b66aed5e23d923674edfa3c62b4e47f5ce3e81e"
},
"pipfile-spec": 6,
"requires": {},
@@ -19,6 +19,7 @@
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.0.6"
},
"argh": {
@@ -31,10 +32,10 @@
},
"bcbio-gff": {
"hashes": [
- "sha256:74c6920c91ca18ed9cb872e9471c0be442dad143d8176345917eb1fefc86bc37"
+ "sha256:34dfa970e14f4533dc63c0a5512b7b5221e4a06449e6aaa344162ed5fdd7a1de"
],
"index": "pypi",
- "version": "==0.6.6"
+ "version": "==0.6.9"
},
"billiard": {
"hashes": [
@@ -45,42 +46,42 @@
},
"bioblend": {
"hashes": [
- "sha256:a362a251a9429f17713bda51b29ccd3f4616a7613eedeb23e828307afb93eb34",
- "sha256:ed37c858c890c41aff41da071838b92bec77a74f1b8d9e0562d40936ce5c255a"
+ "sha256:057450d39054cf91fff31e9025f269eb08e1ef1b437d71dfc73957e7cb0d8195",
+ "sha256:814312e3583a4cbb4ffaa1fb103107d9a24c069604abf51c670251fdc8bf094a"
],
"index": "pypi",
- "version": "==0.15.0"
+ "version": "==0.16.0"
},
"biopython": {
"hashes": [
- "sha256:010142a8ec2549ff0649edd497658964ef1a18eefdb9fd942ec1e81b292ce2d9",
- "sha256:0b9fbb0d3022dc22716da108b8a81b80d952cd97ac1f106de491dce850f92f62",
- "sha256:0df5cddef2819c975e6508adf5d85aa046e449df5420d02b04871c7836b41273",
- "sha256:194528eda6856a4c68f840ca0bcc9b544a5edee3548b97521084e7ac38c833ca",
- "sha256:195f099c2c0c39518b6df921ab2b3cc43a601896018fc61909ac8385d5878866",
- "sha256:1df0bce7fd5e2414d6e18c9229fa0056914d2b9041531c71cac48f38a622142d",
- "sha256:1ee0a0b6c2376680fea6642d5080baa419fd73df104a62d58a8baf7a8bbe4564",
- "sha256:2bd5a630be2a8e593094f7b1717fc962eda8931b68542b97fbf9bd8e2ac1e08d",
- "sha256:4565c97fab16c5697d067b821b6a1da0ec3ef36a9c96cf103ac7b4a94eb9f9ba",
- "sha256:48d424453a5512a1d1d41a4acabdfe5291da1f491a2d3606f2b0e4fbd63aeda6",
- "sha256:5c0b369f91a76b8e5e36624d075585c3f0f088ea4a6e3d015c48f08e48ce0114",
- "sha256:639461a1ac5765406ec8ab8ed619845351f2ff22fed734d86e09e4a7b7719a08",
- "sha256:6ed345b1ef100d58d8376e31c280b13fc87bb8f73ccc447f8140344991b61459",
- "sha256:75b55000793f6b76334b8e80dc7e6d8cd2b019af917aa431cea6646e8e696c7f",
- "sha256:9b4374a47d924d4d4ffe2fea010ce75427bbfd92e45d50d5b1213a478baf680f",
- "sha256:ada611f12ee3b0bef7308ef41ee7b94898613b369ab44e0268d74bd1d6a06920",
- "sha256:b470c44d7a04e40a0cfc65853b1a5a6bf506a130c334cf4cffa05df07dbda366",
- "sha256:c130c8e64ae2e4c7c73f0c24974ac8a832190cc3cf3c3c7b4aaffc974effc993",
- "sha256:cc3b0b78022d14f11d508038a288a189d03c97c476d6636c7b6f98bd8bc8462b",
- "sha256:cfb93842501ebc0e0ef6520daddcbeeefc9b61736972580917dafd5c8a5a8041",
- "sha256:d15d09bfe0d3a8a416a596a3909d9718c811df852d969592b4fa9e0da9cf7375",
- "sha256:e0af107cc62a905d13d35dd7b38f335a37752ede45e4617139e84409a6a88dc4",
- "sha256:f1076653937947773768455556b1d24acad9575759e9089082f32636b09add54",
- "sha256:f5021a398c898b9cf6815cc5171c146a601b935b55364c53e6516a2545ab740c",
- "sha256:fe2bcf85d0f5f1888ed7d86c139e9d4e7d54e036c8ac54e929663d63548046a1"
+ "sha256:03ee5c72b3cc3f0675a8c22ce1c45fe99a32a60db18df059df479ae6cf619708",
+ "sha256:155c5b95857bca7ebd607210cb9d8ea459bb0b86b3ca37ea44ec47c26ede7e9a",
+ "sha256:2dbb4388c75b5dfca8ce729e791f465c9c878dbd7ba2ab9a1f9854609d2b5426",
+ "sha256:365569543ea58dd07ef205ec351c23b6c1a3200d5d321eb28ceaecd55eb5955e",
+ "sha256:4b3d4eec2e348c3d97a7fde80ee0f2b8ebeed849d2bd64a616833a9be03b93c8",
+ "sha256:4be31815226052d86d4c2f6a103c40504e34bba3e25cc1b1d687a3203c42fb6e",
+ "sha256:51eb467a60c38820ad1e6c3a7d4cb10535606f559646e824cc65c96091d91ff7",
+ "sha256:5ae69c5e09769390643aa0f8064517665df6fb99c37433821d6664584d0ecb8c",
+ "sha256:72a1477cf1701964c7224e506a54fd65d1cc5228da200b634a17992230aa1cbd",
+ "sha256:76988ed3d7383d566db1d7fc69c9cf136c6275813fb749fc6753c340f81f1a8f",
+ "sha256:83bfea8a19f9352c47b13965c4b73853e7aeef3c5aed8489895b0679e32c621b",
+ "sha256:884a2b99ac7820cb84f70089769a512e3238ee60438b8c934ed519613dc570ce",
+ "sha256:8f33dafd3c7254fff5e1684b965e45a7c08d9b8e1bf51562b0a521ff9a6f5ea0",
+ "sha256:947b793e804c59ea45ae46945a57612ad1789ca87af4af0d6a62dcecf3a6246a",
+ "sha256:9580978803b582e0612b71673cab289e6bf261a865009cfb9501d65bc726a76e",
+ "sha256:98deacc30b8654cfcdcf707d93fa4e3c8717bbda07c3f9f828cf84753d4a1e4d",
+ "sha256:aa23a83a220486af6193760d079b36543fe00afcfbd18280ca2fd0b2c1c8dd6d",
+ "sha256:ab93d5749b375be3682866b3a606aa2ebd3e6d868079793925bf4fbb0987cf1f",
+ "sha256:b3ab26f26a1956ef26303386510d84e917e31fcbbc94918c336da0163ef628df",
+ "sha256:bf634a56f449a4123e48e538d661948e5ac29fb452acd2962b8cb834b472a9d7",
+ "sha256:ceab668be9cbdcddef55ad459f87acd0316ae4a00d32251fea4cf665f5062fda",
+ "sha256:d9f6ce961e0c380e2a5435f64c96421dbcebeab6a1b41506bd81251feb733c08",
+ "sha256:e921571b51514a6d35944242d6fef6427c3998acf58940fe1f209ac8a92a6e87",
+ "sha256:edb07eac99d3b8abd7ba56ff4bedec9263f76dfc3c3f450e7d2e2bcdecf8559b",
+ "sha256:f0a7e1d94a318f74974345fd0987ec389b16988ec484e67218e900b116b932a8"
],
"index": "pypi",
- "version": "==1.78"
+ "version": "==1.79"
},
"blinker": {
"hashes": [
@@ -95,6 +96,14 @@
],
"version": "==2.49.0"
},
+ "cached-property": {
+ "hashes": [
+ "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130",
+ "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"
+ ],
+ "markers": "python_version < '3.8'",
+ "version": "==1.5.2"
+ },
"celery": {
"hashes": [
"sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
@@ -105,30 +114,34 @@
},
"certifi": {
"hashes": [
- "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
- "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
+ "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
+ "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
- "version": "==2020.12.5"
+ "version": "==2021.10.8"
},
- "chardet": {
+ "charset-normalizer": {
"hashes": [
- "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
- "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
+ "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
+ "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
],
- "version": "==4.0.0"
+ "markers": "python_version >= '3'",
+ "version": "==2.0.9"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"click-didyoumean": {
"hashes": [
- "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"
+ "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
+ "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
],
- "version": "==0.0.3"
+ "markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'",
+ "version": "==0.3.0"
},
"click-plugins": {
"hashes": [
@@ -139,26 +152,34 @@
},
"click-repl": {
"hashes": [
- "sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5",
- "sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5"
+ "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b",
+ "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"
],
- "version": "==0.1.6"
+ "version": "==0.2.0"
},
"configparser": {
"hashes": [
- "sha256:85d5de102cfe6d14a5172676f09d19c465ce63d6019cf0a4ef13385fc535e828",
- "sha256:af59f2cdd7efbdd5d111c1976ecd0b82db9066653362f0962d7bf1d3ab89a1fa"
+ "sha256:1b35798fdf1713f1c3139016cfcbc461f09edbf099d1fb658d4b7479fcaa3daa",
+ "sha256:e8b39238fb6f0153a069aa253d349467c3c4737934f253ef6abac5fe0eca1e5d"
],
"index": "pypi",
- "version": "==5.0.2"
+ "version": "==5.2.0"
},
"deepdiff": {
"hashes": [
- "sha256:dd79b81c2d84bfa33aa9d94d456b037b68daff6bb87b80dfaa1eca04da68b349",
- "sha256:e054fed9dfe0d83d622921cbb3a3d0b3a6dd76acd2b6955433a0a2d35147774a"
+ "sha256:e3f1c3a375c7ea5ca69dba6f7920f9368658318ff1d8a496293c79481f48e649",
+ "sha256:ef3410ca31e059a9d10edfdff552245829835b3ecd03212dc5b533d45a6c3f57"
],
"index": "pypi",
- "version": "==5.5.0"
+ "version": "==5.6.0"
+ },
+ "deprecated": {
+ "hashes": [
+ "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d",
+ "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.2.13"
},
"flask": {
"hashes": [
@@ -178,18 +199,19 @@
},
"gitdb": {
"hashes": [
- "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
- "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
+ "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd",
+ "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"
],
- "version": "==4.0.7"
+ "markers": "python_version >= '3.6'",
+ "version": "==4.0.9"
},
"gitpython": {
"hashes": [
- "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135",
- "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"
+ "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647",
+ "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"
],
"index": "pypi",
- "version": "==3.1.17"
+ "version": "==3.1.24"
},
"gunicorn": {
"hashes": [
@@ -201,18 +223,19 @@
},
"idna": {
"hashes": [
- "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
- "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
+ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
+ "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
- "version": "==2.10"
+ "markers": "python_version >= '3'",
+ "version": "==3.3"
},
"importlib-metadata": {
"hashes": [
- "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581",
- "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"
+ "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100",
+ "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"
],
"markers": "python_version < '3.8'",
- "version": "==4.0.1"
+ "version": "==4.8.2"
},
"isodate": {
"hashes": [
@@ -226,6 +249,7 @@
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"jinja2": {
@@ -233,117 +257,176 @@
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.3"
},
"kombu": {
"hashes": [
- "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
- "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
+ "sha256:0f5d0763fb916808f617b886697b2be28e6bc35026f08e679697fc814b48a608",
+ "sha256:d36f0cde6a18d9eb7b6b3aa62a59bfdff7f5724689850e447eca5be8efc9d501"
],
- "version": "==5.0.2"
+ "markers": "python_version >= '3.7'",
+ "version": "==5.2.2"
},
"markupsafe": {
"hashes": [
- "sha256:007dc055dbce5b1104876acee177dbfd18757e19d562cd440182e1f492e96b95",
- "sha256:031bf79a27d1c42f69c276d6221172417b47cb4b31cdc73d362a9bf5a1889b9f",
- "sha256:161d575fa49395860b75da5135162481768b11208490d5a2143ae6785123e77d",
- "sha256:24bbc3507fb6dfff663af7900a631f2aca90d5a445f272db5fc84999fa5718bc",
- "sha256:2efaeb1baff547063bad2b2893a8f5e9c459c4624e1a96644bbba08910ae34e0",
- "sha256:32200f562daaab472921a11cbb63780f1654552ae49518196fc361ed8e12e901",
- "sha256:3261fae28155e5c8634dd7710635fe540a05b58f160cef7713c7700cb9980e66",
- "sha256:3b54a9c68995ef4164567e2cd1a5e16db5dac30b2a50c39c82db8d4afaf14f63",
- "sha256:3c352ff634e289061711608f5e474ec38dbaa21e3e168820d53d5f4015e5b91b",
- "sha256:3fb47f97f1d338b943126e90b79cad50d4fcfa0b80637b5a9f468941dbbd9ce5",
- "sha256:441ce2a8c17683d97e06447fcbccbdb057cbf587c78eb75ae43ea7858042fe2c",
- "sha256:45535241baa0fc0ba2a43961a1ac7562ca3257f46c4c3e9c0de38b722be41bd1",
- "sha256:4aca81a687975b35e3e80bcf9aa93fe10cd57fac37bf18b2314c186095f57e05",
- "sha256:4cc563836f13c57f1473bc02d1e01fc37bab70ad4ee6be297d58c1d66bc819bf",
- "sha256:4fae0677f712ee090721d8b17f412f1cbceefbf0dc180fe91bab3232f38b4527",
- "sha256:58bc9fce3e1557d463ef5cee05391a05745fd95ed660f23c1742c711712c0abb",
- "sha256:664832fb88b8162268928df233f4b12a144a0c78b01d38b81bdcf0fc96668ecb",
- "sha256:70820a1c96311e02449591cbdf5cd1c6a34d5194d5b55094ab725364375c9eb2",
- "sha256:79b2ae94fa991be023832e6bcc00f41dbc8e5fe9d997a02db965831402551730",
- "sha256:83cf0228b2f694dcdba1374d5312f2277269d798e65f40344964f642935feac1",
- "sha256:87de598edfa2230ff274c4de7fcf24c73ffd96208c8e1912d5d0fee459767d75",
- "sha256:8f806bfd0f218477d7c46a11d3e52dc7f5fdfaa981b18202b7dc84bbc287463b",
- "sha256:90053234a6479738fd40d155268af631c7fca33365f964f2208867da1349294b",
- "sha256:a00dce2d96587651ef4fa192c17e039e8cfab63087c67e7d263a5533c7dad715",
- "sha256:a08cd07d3c3c17cd33d9e66ea9dee8f8fc1c48e2d11bd88fd2dc515a602c709b",
- "sha256:a19d39b02a24d3082856a5b06490b714a9d4179321225bbf22809ff1e1887cc8",
- "sha256:d00a669e4a5bec3ee6dbeeeedd82a405ced19f8aeefb109a012ea88a45afff96",
- "sha256:dab0c685f21f4a6c95bfc2afd1e7eae0033b403dd3d8c1b6d13a652ada75b348",
- "sha256:df561f65049ed3556e5b52541669310e88713fdae2934845ec3606f283337958",
- "sha256:e4570d16f88c7f3032ed909dc9e905a17da14a1c4cfd92608e3fda4cb1208bbd",
- "sha256:e77e4b983e2441aff0c0d07ee711110c106b625f440292dfe02a2f60c8218bd6",
- "sha256:e79212d09fc0e224d20b43ad44bb0a0a3416d1e04cf6b45fed265114a5d43d20",
- "sha256:f58b5ba13a5689ca8317b98439fccfbcc673acaaf8241c1869ceea40f5d585bf",
- "sha256:fef86115fdad7ae774720d7103aa776144cf9b66673b4afa9bcaa7af990ed07b"
- ],
- "version": "==2.0.0"
+ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
+ "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
+ "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
+ "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194",
+ "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
+ "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
+ "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724",
+ "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
+ "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646",
+ "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
+ "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6",
+ "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a",
+ "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6",
+ "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad",
+ "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
+ "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38",
+ "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac",
+ "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
+ "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6",
+ "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047",
+ "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
+ "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
+ "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b",
+ "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
+ "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
+ "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a",
+ "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
+ "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1",
+ "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9",
+ "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864",
+ "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
+ "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee",
+ "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f",
+ "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
+ "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
+ "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
+ "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
+ "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b",
+ "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
+ "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86",
+ "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6",
+ "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
+ "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
+ "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
+ "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28",
+ "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e",
+ "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
+ "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
+ "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f",
+ "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d",
+ "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
+ "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
+ "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145",
+ "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
+ "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c",
+ "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1",
+ "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a",
+ "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207",
+ "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
+ "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53",
+ "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd",
+ "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134",
+ "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85",
+ "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9",
+ "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
+ "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
+ "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
+ "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
+ "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2.0.1"
},
"numpy": {
"hashes": [
- "sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94",
- "sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080",
- "sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e",
- "sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c",
- "sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76",
- "sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371",
- "sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c",
- "sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2",
- "sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a",
- "sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb",
- "sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140",
- "sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28",
- "sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f",
- "sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d",
- "sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff",
- "sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8",
- "sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa",
- "sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea",
- "sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc",
- "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73",
- "sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d",
- "sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d",
- "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4",
- "sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c",
- "sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e",
- "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea",
- "sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd",
- "sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f",
- "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff",
- "sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e",
- "sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7",
- "sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa",
- "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827",
- "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60"
- ],
- "version": "==1.19.5"
+ "sha256:0b78ecfa070460104934e2caf51694ccd00f37d5e5dbe76f021b1b0b0d221823",
+ "sha256:1247ef28387b7bb7f21caf2dbe4767f4f4175df44d30604d42ad9bd701ebb31f",
+ "sha256:1403b4e2181fc72664737d848b60e65150f272fe5a1c1cbc16145ed43884065a",
+ "sha256:170b2a0805c6891ca78c1d96ee72e4c3ed1ae0a992c75444b6ab20ff038ba2cd",
+ "sha256:2e4ed57f45f0aa38beca2a03b6532e70e548faf2debbeb3291cfc9b315d9be8f",
+ "sha256:32fe5b12061f6446adcbb32cf4060a14741f9c21e15aaee59a207b6ce6423469",
+ "sha256:34f3456f530ae8b44231c63082c8899fe9c983fd9b108c997c4b1c8c2d435333",
+ "sha256:4c9c23158b87ed0e70d9a50c67e5c0b3f75bcf2581a8e34668d4e9d7474d76c6",
+ "sha256:5d95668e727c75b3f5088ec7700e260f90ec83f488e4c0aaccb941148b2cd377",
+ "sha256:615d4e328af7204c13ae3d4df7615a13ff60a49cb0d9106fde07f541207883ca",
+ "sha256:69077388c5a4b997442b843dbdc3a85b420fb693ec8e33020bb24d647c164fa5",
+ "sha256:74b85a17528ca60cf98381a5e779fc0264b4a88b46025e6bcbe9621f46bb3e63",
+ "sha256:81225e58ef5fce7f1d80399575576fc5febec79a8a2742e8ef86d7b03beef49f",
+ "sha256:8890b3360f345e8360133bc078d2dacc2843b6ee6059b568781b15b97acbe39f",
+ "sha256:92aafa03da8658609f59f18722b88f0a73a249101169e28415b4fa148caf7e41",
+ "sha256:9864424631775b0c052f3bd98bc2712d131b3e2cd95d1c0c68b91709170890b0",
+ "sha256:9e6f5f50d1eff2f2f752b3089a118aee1ea0da63d56c44f3865681009b0af162",
+ "sha256:a3deb31bc84f2b42584b8c4001c85d1934dbfb4030827110bc36bfd11509b7bf",
+ "sha256:ad010846cdffe7ec27e3f933397f8a8d6c801a48634f419e3d075db27acf5880",
+ "sha256:b1e2312f5b8843a3e4e8224b2b48fe16119617b8fc0a54df8f50098721b5bed2",
+ "sha256:bc988afcea53e6156546e5b2885b7efab089570783d9d82caf1cfd323b0bb3dd",
+ "sha256:c449eb870616a7b62e097982c622d2577b3dbc800aaf8689254ec6e0197cbf1e",
+ "sha256:c74c699b122918a6c4611285cc2cad4a3aafdb135c22a16ec483340ef97d573c",
+ "sha256:c885bfc07f77e8fee3dc879152ba993732601f1f11de248d4f357f0ffea6a6d4",
+ "sha256:e3c3e990274444031482a31280bf48674441e0a5b55ddb168f3a6db3e0c38ec8",
+ "sha256:e4799be6a2d7d3c33699a6f77201836ac975b2e1b98c2a07f66a38f499cb50ce",
+ "sha256:e6c76a87633aa3fa16614b61ccedfae45b91df2767cf097aa9c933932a7ed1e0",
+ "sha256:e89717274b41ebd568cd7943fc9418eeb49b1785b66031bc8a7f6300463c5898",
+ "sha256:f5162ec777ba7138906c9c274353ece5603646c6965570d82905546579573f73",
+ "sha256:fde96af889262e85aa033f8ee1d3241e32bf36228318a61f1ace579df4e8170d"
+ ],
+ "markers": "python_version < '3.11' and python_version >= '3.7'",
+ "version": "==1.21.4"
},
"ordered-set": {
"hashes": [
"sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"
],
+ "markers": "python_version >= '3.5'",
"version": "==4.0.2"
},
"prompt-toolkit": {
"hashes": [
- "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
- "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
+ "sha256:5f29d62cb7a0ecacfa3d8ceea05a63cd22500543472d64298fc06ddda906b25d",
+ "sha256:7053aba00895473cb357819358ef33f11aa97e4ac83d38efb123e5649ceeecaf"
],
- "version": "==3.0.18"
+ "markers": "python_full_version >= '3.6.2'",
+ "version": "==3.0.23"
},
"pyasn1": {
"hashes": [
+ "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
+ "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
+ "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
+ "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
- "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
+ "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
+ "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
+ "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
+ "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
+ "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
+ "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
+ "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
+ "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"pyasn1-modules": {
"hashes": [
+ "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
+ "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
+ "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
+ "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
+ "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
- "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
+ "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
+ "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
+ "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
+ "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
+ "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
+ "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
+ "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
],
"version": "==0.2.8"
},
@@ -356,111 +439,126 @@
},
"pyparsing": {
"hashes": [
- "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
- "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
+ "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
+ "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
],
- "version": "==2.4.7"
+ "markers": "python_version >= '3.6'",
+ "version": "==3.0.6"
},
"pysam": {
"hashes": [
- "sha256:107eca9050d8140910b5ea5c5a9e66313e1155eb44cc180e10f48f9cb71e8095",
- "sha256:3eb70111a5ed86cc7a048c9b2205087b1184093e6135f02ad7f144b206951452",
- "sha256:9da29490c666a963e5a7f6f5114e86c9b36a8a6adc2227f5772bdc38c09d2d37",
- "sha256:9e3597a49e4bc72c31199d6231018ad3034e08a8243b9f8086953afb2ab5a3af",
- "sha256:a5a0fc1f0d724d0b7789341add26ba181ac009430021f0998f6083fb62432193",
- "sha256:d428a9768691d5ea3c28cc52a949c920ae691aa4c110a8b7328dc4d165ef1ad6",
- "sha256:f65659deadc4904984de24cb6f3878b6052cf504a2a85a50b80f2ff7939f96db"
+ "sha256:0cfa16f76ed3c3119c7b3c8dfdcba9e010fbcdcf87eaa165351bb369da5a6bf1",
+ "sha256:1d6d49a0b3c626fae410a93d4c80583a8b5ddaacc9b46a080b250dbcebd30a59",
+ "sha256:2717509556fecddf7c73966fa62066c6a59a7d39b755d8972afa8d143a1d5aa5",
+ "sha256:493988420db16e6ee03393518e4d272df05f0a35780248c08c61da7411e520e7",
+ "sha256:7a8a25fceaaa96e5b4c8b0a7fd6bb0b20b6c262dc4cc867c6d1467ac990f1d77",
+ "sha256:7ea2e019294e4bf25e4892b5de69c43f54fb6ac42b681265268aa322e1f36f5b",
+ "sha256:7f6a4ec58ad7995b791a71bf35f673ea794e734c587ea7329fca5cce9c53a7af",
+ "sha256:9422c2d0b581c3d24f247c15bb8981569e636003c4d6cad39ccd1bf205a79f2c",
+ "sha256:a88f875114bd3d8efb7fade80e0640094383ec5043861aa575175fa9a56edf90",
+ "sha256:c90341434e7a99439174aa64ca5406f63528be4217d4401fb30ec4ea4629c559",
+ "sha256:ca0c9289dfdc5e1a81bccdb8305192cd14cf9730bd21320ceca949fde071a572",
+ "sha256:cfb162358c5284b31b2b88b10947e0f1013da2d85ba0fd0b5723dd142c15329e",
+ "sha256:cfffad99cf3968cf85aadb70a8a02303f9172ea21abe02d587c44f808c504f52",
+ "sha256:e13e496da3a432db24f424439834b0ab5f40700a3db6e610d06f8bd639d9fd2d",
+ "sha256:ef5d8ad01cac8974cd09832c226cbb63a3f7c5bd63727d8e59447021ee16a186",
+ "sha256:f5a23a5dcf32f01c66d44e89113fa8f7522997ea43fbc0f98e5250a907911a5f"
],
"index": "pypi",
- "version": "==0.16.0.1"
+ "version": "==0.18.0"
},
"python-dateutil": {
"hashes": [
- "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
- "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"index": "pypi",
- "version": "==2.8.1"
+ "version": "==2.8.2"
},
"python-ldap": {
"hashes": [
- "sha256:4711cacf013e298754abd70058ccc995758177fb425f1c2d30e71adfc1d00aa5"
+ "sha256:60464c8fc25e71e0fd40449a24eae482dcd0fb7fcf823e7de627a6525b3e0d12"
],
"index": "pypi",
- "version": "==3.3.1"
+ "version": "==3.4.0"
},
"python-magic": {
"hashes": [
- "sha256:8551e804c09a3398790bd9e392acb26554ae2609f29c72abb0b9dee9a5571eae",
- "sha256:ca884349f2c92ce830e3f498c5b7c7051fe2942c3ee4332f65213b8ebff15a62"
+ "sha256:4fec8ee805fea30c07afccd1592c0f17977089895bdfaae5fec870a84e997626",
+ "sha256:de800df9fb50f8ec5974761054a708af6e4246b03b4bdaee993f948947b0ebcf"
],
"index": "pypi",
- "version": "==0.4.22"
+ "version": "==0.4.24"
},
"pytz": {
"hashes": [
- "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
- "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
+ "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c",
+ "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"
],
- "version": "==2021.1"
+ "version": "==2021.3"
},
"pyyaml": {
"hashes": [
- "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
- "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
- "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
- "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
- "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
- "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
- "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
- "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
- "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
- "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
- "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
- "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
- "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
- "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
- "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
- "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
- "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
- "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
- "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
- "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
- "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
- "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
- "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
- "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
- "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
- "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
- "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
- "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
- "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
- ],
- "version": "==5.4.1"
+ "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
+ "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
+ "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
+ "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
+ "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
+ "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
+ "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
+ "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
+ "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
+ "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
+ "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
+ "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
+ "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
+ "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
+ "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
+ "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
+ "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
+ "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
+ "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
+ "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
+ "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
+ "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
+ "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
+ "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
+ "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
+ "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
+ "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
+ "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
+ "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
+ "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
+ "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
+ "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
+ "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==6.0"
},
"rdflib": {
"hashes": [
- "sha256:78149dd49d385efec3b3adfbd61c87afaf1281c30d3fcaf1b323b34f603fb155",
- "sha256:88208ea971a87886d60ae2b1a4b2cdc263527af0454c422118d43fe64b357877"
+ "sha256:6136ae056001474ee2aff5fc5b956e62a11c3a9c66bb0f3d9c0aaa5fbb56854e",
+ "sha256:b7642daac8cdad1ba157fecb236f5d1b2aa1de64e714dcee80d65e2b794d88a6"
],
"index": "pypi",
- "version": "==5.0.0"
+ "version": "==6.0.2"
},
"redis": {
"hashes": [
- "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
- "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
+ "sha256:c8481cf414474e3497ec7971a1ba9b998c8efad0f0d289a009a5bbef040894f9",
+ "sha256:ccf692811f2c1fc7a92b466aa2599e4a6d2d73d5f736a2c70be600657c0da34a"
],
"index": "pypi",
- "version": "==3.5.3"
+ "version": "==4.0.2"
},
"requests": {
"hashes": [
- "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
- "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
+ "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
+ "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
],
"index": "pypi",
- "version": "==2.25.1"
+ "version": "==2.26.0"
},
"requests-toolbelt": {
"hashes": [
@@ -474,29 +572,49 @@
"flask"
],
"hashes": [
- "sha256:c1227d38dca315ba35182373f129c3e2722e8ed999e52584e6aca7d287870739",
- "sha256:c7d380a21281e15be3d9f67a3c4fbb4f800c481d88ff8d8931f39486dd7b4ada"
+ "sha256:0db297ab32e095705c20f742c3a5dac62fe15c4318681884053d0898e5abb2f6",
+ "sha256:789a11a87ca02491896e121efdd64e8fd93327b69e8f2f7d42f03e2569648e88"
],
"index": "pypi",
- "version": "==1.1.0"
+ "version": "==1.5.0"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:6d10741ff20b89cd8c6a536ee9dc90d3002dec0226c78fb98605bfb9ef8a7adf",
+ "sha256:d144f85102f999444d06f9c0e8c737fd0194f10f2f7e5fdb77573f6e2fa4fad0"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==59.5.0"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:b4c634615a0cf5b02cf83c7bedffc8da0ca439f00e79452699454da6fbd4153d",
+ "sha256:feb5ff19b354cde9efd2344ef6d5e79880ce4be643037641b49508bbb850d060"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==59.4.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"smmap": {
"hashes": [
- "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
- "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
+ "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94",
+ "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"
],
- "version": "==4.0.0"
+ "markers": "python_version >= '3.6'",
+ "version": "==5.0.0"
},
"sparqlwrapper": {
"hashes": [
+ "sha256:17ec44b08b8ae2888c801066249f74fe328eec25d90203ce7eadaf82e64484c7",
"sha256:357ee8a27bc910ea13d77836dbddd0b914991495b8cc1bf70676578155e962a8",
+ "sha256:8cf6c21126ed76edc85c5c232fd6f77b9f61f8ad1db90a7147cdde2104aff145",
"sha256:c7f9c9d8ebb13428771bc3b6dee54197422507dcc3dea34e30d5dcfc53478dec",
"sha256:d6a66b5b8cda141660e07aeb00472db077a98d22cb588c973209c7336850fb3c"
],
@@ -505,32 +623,32 @@
},
"tld": {
"hashes": [
- "sha256:1a69b2cd4053da5377a0b27e048e97871120abf9cd7a62ff270915d0c11369d6",
- "sha256:1b63094d893657eadfd61e49580b4225ce958ca3b8013dbb9485372cde5a3434",
- "sha256:3266e6783825a795244a0ed225126735e8121859113b0a7fc830cc49f7bbdaff",
- "sha256:478d9b23157c7e3e2d07b0534da3b1e61a619291b6e3f52f5a3510e43acec7e9",
- "sha256:5bd36b24aeb14e766ef1e5c01b96fe89043db44a579848f716ec03c40af50a6b",
- "sha256:cf1b7af4c1d9c689ca81ea7cf3cae77d1bfd8aaa4c648b58f76a0b3d32e3f6e0",
- "sha256:d5938730cdb9ce4b0feac4dc887d971f964dba873a74ad818f0f25c1571c6045"
+ "sha256:266106ad9035f54cd5cce5f823911a51f697e7c58cb45bfbd6c53b4c2976ece2",
+ "sha256:69fed19d26bb3f715366fb4af66fdeace896c55c052b00e8aaba3a7b63f3e7f0",
+ "sha256:826bbe61dccc8d63144b51caef83e1373fbaac6f9ada46fca7846021f5d36fef",
+ "sha256:843844e4256c943983d86366b5af3ac9cd1c9a0b6465f04d9f70e3b4c1a7989f",
+ "sha256:a92ac6b84917e7d9e934434b8d37e9be534598f138fbb86b3c0d5426f2621890",
+ "sha256:b6650f2d5392a49760064bc55d73ce3397a378ef24ded96efb516c6b8ec68c26",
+ "sha256:ef5b162d6fa295822dacd4fe4df1b62d8df2550795a97399a8905821b58d3702"
],
"index": "pypi",
- "version": "==0.12.5"
+ "version": "==0.12.6"
},
"typing-extensions": {
"hashes": [
- "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
- "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
- "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
+ "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
+ "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
],
- "markers": "python_version < '3.8'",
- "version": "==3.10.0.0"
+ "markers": "python_version < '3.10'",
+ "version": "==4.0.1"
},
"urllib3": {
"hashes": [
- "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
- "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
+ "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
+ "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
],
- "version": "==1.26.4"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'",
+ "version": "==1.26.7"
},
"validate-email": {
"hashes": [
@@ -544,30 +662,37 @@
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.0.0"
},
"watchdog": {
"hashes": [
- "sha256:027c532e2fd3367d55fe235510fc304381a6cc88d0dcd619403e57ffbd83c1d2",
- "sha256:12645d41d7307601b318c48861e776ce7a9fdcad9f74961013ec39037050582c",
- "sha256:16078cd241a95124acd4d8d3efba2140faec9300674b12413cc08be11b825d56",
- "sha256:20d4cabfa2ad7239995d81a0163bc0264a3e104a64f33c6f0a21ad75a0d915d9",
- "sha256:22c13c19599b0dec7192f8f7d26404d5223cb36c9a450e96430483e685dccd7e",
- "sha256:2894440b4ea95a6ef4c5d152deedbe270cae46092682710b7028a04d6a6980f6",
- "sha256:4d83c89ba24bd67b7a7d5752a4ef953ec40db69d4d30582bd1f27d3ecb6b61b0",
- "sha256:5b391bac7edbdf96fb82a381d04829bbc0d1bb259c206b2b283ef8989340240f",
- "sha256:604ca364a79c27a694ab10947cd41de81bf229cff507a3156bf2c56c064971a1",
- "sha256:67c645b1e500cc74d550e9aad4829309c5084dc55e8dc4e1c25d5da23e5be239",
- "sha256:9f1b124fe2d4a1f37b7068f6289c2b1eba44859eb790bf6bd709adff224a5469",
- "sha256:a1b3f76e2a0713b406348dd5b9df2aa02bdd741a6ddf54f4c6410b395e077502",
- "sha256:a9005f968220b715101d5fcdde5f5deda54f0d1873f618724f547797171f5e97",
- "sha256:aa59afc87a892ed92d7d88d09f4b736f1336fc35540b403da7ee00c3be74bd07",
- "sha256:c1325b47463fce231d88eb74f330ab0cb4a1bab5defe12c0c80a3a4f197345b4",
- "sha256:dca75d12712997c713f76e6d68ff41580598c7df94cedf83f1089342e7709081",
- "sha256:f3edbe1e15e229d2ba8ff5156908adba80d1ba21a9282d9f72247403280fc799"
+ "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685",
+ "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04",
+ "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb",
+ "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542",
+ "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6",
+ "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b",
+ "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660",
+ "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3",
+ "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923",
+ "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7",
+ "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b",
+ "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669",
+ "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2",
+ "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3",
+ "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604",
+ "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8",
+ "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5",
+ "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0",
+ "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6",
+ "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65",
+ "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d",
+ "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15",
+ "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"
],
"index": "pypi",
- "version": "==2.1.1"
+ "version": "==2.1.6"
},
"wcwidth": {
"hashes": [
@@ -584,12 +709,70 @@
"index": "pypi",
"version": "==0.16.1"
},
+ "wrapt": {
+ "hashes": [
+ "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179",
+ "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096",
+ "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374",
+ "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df",
+ "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185",
+ "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785",
+ "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7",
+ "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909",
+ "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918",
+ "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33",
+ "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068",
+ "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829",
+ "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af",
+ "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79",
+ "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce",
+ "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc",
+ "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36",
+ "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade",
+ "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca",
+ "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32",
+ "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125",
+ "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e",
+ "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709",
+ "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f",
+ "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b",
+ "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb",
+ "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb",
+ "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489",
+ "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640",
+ "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb",
+ "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851",
+ "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d",
+ "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44",
+ "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13",
+ "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2",
+ "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb",
+ "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b",
+ "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9",
+ "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755",
+ "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c",
+ "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a",
+ "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf",
+ "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3",
+ "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229",
+ "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e",
+ "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de",
+ "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554",
+ "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10",
+ "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80",
+ "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056",
+ "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==1.13.3"
+ },
"zipp": {
"hashes": [
- "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76",
- "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"
+ "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832",
+ "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"
],
- "version": "==3.4.1"
+ "markers": "python_version >= '3.6'",
+ "version": "==3.6.0"
}
},
"develop": {
@@ -598,96 +781,95 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"certifi": {
"hashes": [
- "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
- "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
+ "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
+ "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
- "version": "==2020.12.5"
+ "version": "==2021.10.8"
},
- "chardet": {
+ "charset-normalizer": {
"hashes": [
- "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
- "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
+ "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
+ "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
],
- "version": "==4.0.0"
+ "markers": "python_version >= '3'",
+ "version": "==2.0.9"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"coverage": {
"extras": [
- "toml"
- ],
- "hashes": [
- "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
- "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
- "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
- "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
- "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
- "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
- "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
- "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
- "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
- "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
- "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
- "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
- "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
- "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
- "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
- "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
- "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
- "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
- "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
- "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
- "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
- "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
- "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
- "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
- "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
- "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
- "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
- "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
- "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
- "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
- "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
- "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
- "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
- "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
- "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
- "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
- "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
- "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
- "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
- "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
- "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
- "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
- "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
- "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
- "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
- "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
- "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
- "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
- "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
- "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
- "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
- "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
- ],
- "version": "==5.5"
+
+ ],
+ "hashes": [
+ "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0",
+ "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd",
+ "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884",
+ "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48",
+ "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76",
+ "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0",
+ "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64",
+ "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685",
+ "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47",
+ "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d",
+ "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840",
+ "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f",
+ "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971",
+ "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c",
+ "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a",
+ "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de",
+ "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17",
+ "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4",
+ "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521",
+ "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57",
+ "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b",
+ "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282",
+ "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644",
+ "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475",
+ "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d",
+ "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da",
+ "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953",
+ "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2",
+ "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e",
+ "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c",
+ "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc",
+ "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64",
+ "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74",
+ "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617",
+ "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3",
+ "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d",
+ "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa",
+ "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739",
+ "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8",
+ "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8",
+ "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781",
+ "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58",
+ "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9",
+ "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c",
+ "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd",
+ "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e",
+ "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==6.2"
},
"coveralls": {
"hashes": [
- "sha256:7bd173b3425733661ba3063c88f180127cc2b20e9740686f86d2622b31b41385",
- "sha256:cbb942ae5ef3d2b55388cb5b43e93a269544911535f1e750e1c656aef019ce60"
+ "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea",
+ "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"
],
"index": "pypi",
- "version": "==3.0.1"
+ "version": "==3.3.1"
},
"docopt": {
"hashes": [
@@ -697,32 +879,34 @@
},
"flake8": {
"hashes": [
- "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b",
- "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"
+ "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d",
+ "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"
],
"index": "pypi",
- "version": "==3.9.2"
+ "version": "==4.0.1"
},
- "future": {
+ "ghp-import": {
"hashes": [
- "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
+ "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46",
+ "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"
],
- "version": "==0.18.2"
+ "version": "==2.0.2"
},
"idna": {
"hashes": [
- "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
- "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
+ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
+ "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
- "version": "==2.10"
+ "markers": "python_version >= '3'",
+ "version": "==3.3"
},
"importlib-metadata": {
"hashes": [
- "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581",
- "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"
+ "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100",
+ "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"
],
"markers": "python_version < '3.8'",
- "version": "==4.0.1"
+ "version": "==4.8.2"
},
"iniconfig": {
"hashes": [
@@ -736,76 +920,91 @@
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.3"
},
- "joblib": {
- "hashes": [
- "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7",
- "sha256:feeb1ec69c4d45129954f1b7034954241eedfd6ba39b5e9e4b6883be3332d5e5"
- ],
- "version": "==1.0.1"
- },
- "livereload": {
- "hashes": [
- "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"
- ],
- "version": "==2.6.3"
- },
- "lunr": {
- "extras": [
- "languages"
- ],
- "hashes": [
- "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca",
- "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"
- ],
- "version": "==0.5.8"
- },
"markdown": {
"hashes": [
- "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49",
- "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"
+ "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006",
+ "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"
],
- "version": "==3.3.4"
+ "markers": "python_version >= '3.6'",
+ "version": "==3.3.6"
},
"markupsafe": {
"hashes": [
- "sha256:007dc055dbce5b1104876acee177dbfd18757e19d562cd440182e1f492e96b95",
- "sha256:031bf79a27d1c42f69c276d6221172417b47cb4b31cdc73d362a9bf5a1889b9f",
- "sha256:161d575fa49395860b75da5135162481768b11208490d5a2143ae6785123e77d",
- "sha256:24bbc3507fb6dfff663af7900a631f2aca90d5a445f272db5fc84999fa5718bc",
- "sha256:2efaeb1baff547063bad2b2893a8f5e9c459c4624e1a96644bbba08910ae34e0",
- "sha256:32200f562daaab472921a11cbb63780f1654552ae49518196fc361ed8e12e901",
- "sha256:3261fae28155e5c8634dd7710635fe540a05b58f160cef7713c7700cb9980e66",
- "sha256:3b54a9c68995ef4164567e2cd1a5e16db5dac30b2a50c39c82db8d4afaf14f63",
- "sha256:3c352ff634e289061711608f5e474ec38dbaa21e3e168820d53d5f4015e5b91b",
- "sha256:3fb47f97f1d338b943126e90b79cad50d4fcfa0b80637b5a9f468941dbbd9ce5",
- "sha256:441ce2a8c17683d97e06447fcbccbdb057cbf587c78eb75ae43ea7858042fe2c",
- "sha256:45535241baa0fc0ba2a43961a1ac7562ca3257f46c4c3e9c0de38b722be41bd1",
- "sha256:4aca81a687975b35e3e80bcf9aa93fe10cd57fac37bf18b2314c186095f57e05",
- "sha256:4cc563836f13c57f1473bc02d1e01fc37bab70ad4ee6be297d58c1d66bc819bf",
- "sha256:4fae0677f712ee090721d8b17f412f1cbceefbf0dc180fe91bab3232f38b4527",
- "sha256:58bc9fce3e1557d463ef5cee05391a05745fd95ed660f23c1742c711712c0abb",
- "sha256:664832fb88b8162268928df233f4b12a144a0c78b01d38b81bdcf0fc96668ecb",
- "sha256:70820a1c96311e02449591cbdf5cd1c6a34d5194d5b55094ab725364375c9eb2",
- "sha256:79b2ae94fa991be023832e6bcc00f41dbc8e5fe9d997a02db965831402551730",
- "sha256:83cf0228b2f694dcdba1374d5312f2277269d798e65f40344964f642935feac1",
- "sha256:87de598edfa2230ff274c4de7fcf24c73ffd96208c8e1912d5d0fee459767d75",
- "sha256:8f806bfd0f218477d7c46a11d3e52dc7f5fdfaa981b18202b7dc84bbc287463b",
- "sha256:90053234a6479738fd40d155268af631c7fca33365f964f2208867da1349294b",
- "sha256:a00dce2d96587651ef4fa192c17e039e8cfab63087c67e7d263a5533c7dad715",
- "sha256:a08cd07d3c3c17cd33d9e66ea9dee8f8fc1c48e2d11bd88fd2dc515a602c709b",
- "sha256:a19d39b02a24d3082856a5b06490b714a9d4179321225bbf22809ff1e1887cc8",
- "sha256:d00a669e4a5bec3ee6dbeeeedd82a405ced19f8aeefb109a012ea88a45afff96",
- "sha256:dab0c685f21f4a6c95bfc2afd1e7eae0033b403dd3d8c1b6d13a652ada75b348",
- "sha256:df561f65049ed3556e5b52541669310e88713fdae2934845ec3606f283337958",
- "sha256:e4570d16f88c7f3032ed909dc9e905a17da14a1c4cfd92608e3fda4cb1208bbd",
- "sha256:e77e4b983e2441aff0c0d07ee711110c106b625f440292dfe02a2f60c8218bd6",
- "sha256:e79212d09fc0e224d20b43ad44bb0a0a3416d1e04cf6b45fed265114a5d43d20",
- "sha256:f58b5ba13a5689ca8317b98439fccfbcc673acaaf8241c1869ceea40f5d585bf",
- "sha256:fef86115fdad7ae774720d7103aa776144cf9b66673b4afa9bcaa7af990ed07b"
- ],
- "version": "==2.0.0"
+ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
+ "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
+ "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
+ "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194",
+ "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
+ "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
+ "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724",
+ "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
+ "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646",
+ "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
+ "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6",
+ "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a",
+ "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6",
+ "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad",
+ "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
+ "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38",
+ "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac",
+ "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
+ "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6",
+ "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047",
+ "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
+ "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
+ "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b",
+ "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
+ "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
+ "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a",
+ "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
+ "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1",
+ "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9",
+ "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864",
+ "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
+ "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee",
+ "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f",
+ "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
+ "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
+ "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
+ "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
+ "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b",
+ "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
+ "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86",
+ "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6",
+ "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
+ "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
+ "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
+ "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28",
+ "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e",
+ "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
+ "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
+ "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f",
+ "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d",
+ "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
+ "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
+ "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145",
+ "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
+ "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c",
+ "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1",
+ "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a",
+ "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207",
+ "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
+ "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53",
+ "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd",
+ "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134",
+ "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85",
+ "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9",
+ "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
+ "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
+ "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
+ "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
+ "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2.0.1"
},
"mccabe": {
"hashes": [
@@ -814,172 +1013,155 @@
],
"version": "==0.6.1"
},
- "mkdocs": {
+ "mergedeep": {
"hashes": [
- "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9",
- "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"
+ "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8",
+ "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"
],
- "index": "pypi",
- "version": "==1.1.2"
+ "markers": "python_version >= '3.6'",
+ "version": "==1.3.4"
},
- "nltk": {
+ "mkdocs": {
"hashes": [
- "sha256:240e23ab1ab159ef9940777d30c7c72d7e76d91877099218a7585370c11f6b9e",
- "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb"
+ "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1",
+ "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"
],
- "version": "==3.6.2"
+ "index": "pypi",
+ "version": "==1.2.3"
},
"packaging": {
"hashes": [
- "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
- "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
+ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
+ "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
- "version": "==20.9"
+ "markers": "python_version >= '3.6'",
+ "version": "==21.3"
},
"pluggy": {
"hashes": [
- "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
- "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
+ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
+ "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
],
- "version": "==0.13.1"
+ "markers": "python_version >= '3.6'",
+ "version": "==1.0.0"
},
"py": {
"hashes": [
- "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
- "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
+ "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
+ "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
],
- "version": "==1.10.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==1.11.0"
},
"pycodestyle": {
"hashes": [
- "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
- "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
+ "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20",
+ "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"
],
- "version": "==2.7.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==2.8.0"
},
"pyflakes": {
"hashes": [
- "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3",
- "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"
+ "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c",
+ "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"
],
- "version": "==2.3.1"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==2.4.0"
},
"pyparsing": {
"hashes": [
- "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
- "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
+ "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
+ "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
],
- "version": "==2.4.7"
+ "markers": "python_version >= '3.6'",
+ "version": "==3.0.6"
},
"pytest": {
"hashes": [
- "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
- "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
+ "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
+ "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
],
"index": "pypi",
- "version": "==6.2.4"
+ "version": "==6.2.5"
},
"pytest-cov": {
"hashes": [
- "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e",
- "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e"
+ "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
+ "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
],
"index": "pypi",
- "version": "==2.12.0"
+ "version": "==3.0.0"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ ],
+ "index": "pypi",
+ "version": "==2.8.2"
},
"pyyaml": {
"hashes": [
- "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
- "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
- "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
- "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
- "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
- "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
- "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
- "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
- "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
- "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
- "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
- "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
- "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
- "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
- "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
- "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
- "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
- "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
- "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
- "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
- "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
- "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
- "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
- "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
- "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
- "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
- "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
- "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
- "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
- ],
- "version": "==5.4.1"
- },
- "regex": {
- "hashes": [
- "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5",
- "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79",
- "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31",
- "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500",
- "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11",
- "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14",
- "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3",
- "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439",
- "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c",
- "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82",
- "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711",
- "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093",
- "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a",
- "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb",
- "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8",
- "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17",
- "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000",
- "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d",
- "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480",
- "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc",
- "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0",
- "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9",
- "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765",
- "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e",
- "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a",
- "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07",
- "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f",
- "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac",
- "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7",
- "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed",
- "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968",
- "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7",
- "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2",
- "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4",
- "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87",
- "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8",
- "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10",
- "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29",
- "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605",
- "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6",
- "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"
- ],
- "version": "==2021.4.4"
+ "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
+ "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
+ "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
+ "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
+ "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
+ "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
+ "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
+ "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
+ "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
+ "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
+ "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
+ "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
+ "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
+ "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
+ "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
+ "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
+ "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
+ "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
+ "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
+ "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
+ "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
+ "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
+ "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
+ "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
+ "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
+ "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
+ "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
+ "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
+ "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
+ "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
+ "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
+ "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
+ "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==6.0"
+ },
+ "pyyaml-env-tag": {
+ "hashes": [
+ "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb",
+ "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.1"
},
"requests": {
"hashes": [
- "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
- "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
+ "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
+ "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
],
"index": "pypi",
- "version": "==2.25.1"
+ "version": "==2.26.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"toml": {
@@ -987,83 +1169,68 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
- "tornado": {
- "hashes": [
- "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb",
- "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c",
- "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288",
- "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95",
- "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558",
- "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe",
- "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791",
- "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d",
- "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326",
- "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b",
- "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4",
- "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c",
- "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910",
- "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5",
- "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c",
- "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0",
- "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675",
- "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd",
- "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f",
- "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c",
- "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea",
- "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6",
- "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05",
- "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd",
- "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575",
- "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a",
- "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37",
- "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795",
- "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f",
- "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32",
- "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c",
- "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01",
- "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4",
- "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2",
- "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921",
- "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085",
- "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df",
- "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102",
- "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5",
- "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68",
- "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"
- ],
- "version": "==6.1"
- },
- "tqdm": {
- "hashes": [
- "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3",
- "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae"
- ],
- "version": "==4.60.0"
+ "tomli": {
+ "hashes": [
+ "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee",
+ "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"
+ ],
+ "version": "==1.2.2"
},
"typing-extensions": {
"hashes": [
- "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
- "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
- "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
+ "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
+ "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
],
- "markers": "python_version < '3.8'",
- "version": "==3.10.0.0"
+ "markers": "python_version < '3.10'",
+ "version": "==4.0.1"
},
"urllib3": {
"hashes": [
- "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
- "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
+ "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
+ "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'",
+ "version": "==1.26.7"
+ },
+ "watchdog": {
+ "hashes": [
+ "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685",
+ "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04",
+ "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb",
+ "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542",
+ "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6",
+ "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b",
+ "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660",
+ "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3",
+ "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923",
+ "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7",
+ "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b",
+ "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669",
+ "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2",
+ "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3",
+ "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604",
+ "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8",
+ "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5",
+ "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0",
+ "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6",
+ "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65",
+ "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d",
+ "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15",
+ "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"
],
- "version": "==1.26.4"
+ "index": "pypi",
+ "version": "==2.1.6"
},
"zipp": {
"hashes": [
- "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76",
- "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"
+ "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832",
+ "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"
],
- "version": "==3.4.1"
+ "markers": "python_version >= '3.6'",
+ "version": "==3.6.0"
}
}
}
diff --git a/README.md b/README.md
index a0839fcb..6c863d3e 100644
--- a/README.md
+++ b/README.md
@@ -15,3 +15,4 @@ AskOmics is a visual SPARQL query interface supporting both intuitive data integ
## Documentation
All documentation, included installation instruction is [here](https://flaskomics.readthedocs.io/en/latest/)
+A Galaxy Training tutorial is available [here](https://training.galaxyproject.org/training-material/topics/transcriptomics/tutorials/rna-seq-analysis-with-askomics-it/tutorial.html)
diff --git a/askomics/api/admin.py b/askomics/api/admin.py
index f464182d..5200ebb5 100644
--- a/askomics/api/admin.py
+++ b/askomics/api/admin.py
@@ -7,6 +7,8 @@
from askomics.libaskomics.FilesHandler import FilesHandler
from askomics.libaskomics.LocalAuth import LocalAuth
from askomics.libaskomics.Mailer import Mailer
+from askomics.libaskomics.PrefixManager import PrefixManager
+from askomics.libaskomics.OntologyManager import OntologyManager
from askomics.libaskomics.Result import Result
from askomics.libaskomics.ResultsHandler import ResultsHandler
@@ -256,6 +258,12 @@ def toogle_public_dataset():
datasets_handler.handle_datasets(admin=True)
for dataset in datasets_handler.datasets:
+ if (not data.get("newStatus", False) and dataset.ontology):
+ return jsonify({
+ 'datasets': [],
+ 'error': True,
+ 'errorMessage': "Cannot unpublicize a dataset linked to an ontology"
+ }), 400
current_app.logger.debug(data["newStatus"])
dataset.toggle_public(data["newStatus"], admin=True)
@@ -510,3 +518,305 @@ def delete_datasets():
'error': False,
'errorMessage': ''
})
+
+
+@admin_bp.route("/api/admin/getprefixes", methods=["GET"])
+@api_auth
+@admin_required
+def get_prefixes():
+ """Get all custom prefixes
+
+ Returns
+ -------
+ json
+ prefixes: list of all custom prefixes
+ error: True if error, else False
+ errorMessage: the error message of error, else an empty string
+ """
+ try:
+ pm = PrefixManager(current_app, session)
+ prefixes = pm.get_custom_prefixes()
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ return jsonify({
+ 'prefixes': [],
+ 'error': True,
+ 'errorMessage': str(e)
+ }), 500
+
+ return jsonify({
+ 'prefixes': prefixes,
+ 'error': False,
+ 'errorMessage': ''
+ })
+
+
+@admin_bp.route("/api/admin/addprefix", methods=["POST"])
+@api_auth
+@admin_required
+def add_prefix():
+ """Create a new prefix
+
+ Returns
+ -------
+ json
+ prefixes: list of all custom prefixes
+ error: True if error, else False
+ errorMessage: the error message of error, else an empty string
+ """
+
+ data = request.get_json()
+ if not data or not (data.get("prefix") and data.get("namespace")):
+ return jsonify({
+ 'prefixes': [],
+ 'error': True,
+ 'errorMessage': "Missing parameter"
+ }), 400
+
+ pm = PrefixManager(current_app, session)
+ prefixes = pm.get_custom_prefixes()
+
+ prefix = data.get("prefix")
+ namespace = data.get("namespace")
+
+ if any([prefix == custom['prefix'] for custom in prefixes]):
+ return jsonify({
+ 'prefixes': [],
+ 'error': True,
+ 'errorMessage': "Prefix {} is already in use".format(prefix)
+ }), 400
+
+ try:
+ pm.add_custom_prefix(prefix, namespace)
+ prefixes = pm.get_custom_prefixes()
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ return jsonify({
+ 'prefixes': [],
+ 'error': True,
+ 'errorMessage': str(e)
+ }), 500
+
+ return jsonify({
+ 'prefixes': prefixes,
+ 'error': False,
+ 'errorMessage': ''
+ })
+
+
+@admin_bp.route("/api/admin/delete_prefixes", methods=["POST"])
+@api_auth
+@admin_required
+def delete_prefix():
+ """Delete a prefix
+
+ Returns
+ -------
+ json
+ prefixes: list of all custom prefixes
+ error: True if error, else False
+ errorMessage: the error message of error, else an empty string
+ """
+
+ data = request.get_json()
+ if not data or not data.get("prefixesIdToDelete"):
+ return jsonify({
+ 'prefixes': [],
+ 'error': True,
+ 'errorMessage': "Missing prefixesIdToDelete parameter"
+ }), 400
+
+ pm = PrefixManager(current_app, session)
+ try:
+ pm.remove_custom_prefixes(data.get("prefixesIdToDelete"))
+ prefixes = pm.get_custom_prefixes()
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ return jsonify({
+ 'prefixes': [],
+ 'error': True,
+ 'errorMessage': str(e)
+ }), 500
+
+ return jsonify({
+ 'prefixes': prefixes,
+ 'error': False,
+ 'errorMessage': ''
+ })
+
+
+@admin_bp.route("/api/admin/getontologies", methods=["GET"])
+@api_auth
+@admin_required
+def get_ontologies():
+ """Get all ontologies
+
+ Returns
+ -------
+ json
+ prefixes: list of all custom prefixes
+ error: True if error, else False
+ errorMessage: the error message of error, else an empty string
+ """
+ try:
+ om = OntologyManager(current_app, session)
+ ontologies = om.list_full_ontologies()
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': str(e)
+ }), 500
+
+ return jsonify({
+ 'ontologies': ontologies,
+ 'error': False,
+ 'errorMessage': ''
+ })
+
+
+@admin_bp.route("/api/admin/addontology", methods=["POST"])
+@api_auth
+@admin_required
+def add_ontology():
+ """Create a new ontology
+
+ Returns
+ -------
+ json
+ ontologies: list of all ontologies
+ error: True if error, else False
+ errorMessage: the error message of error, else an empty string
+ """
+
+ data = request.get_json()
+ if not data or not (data.get("name") and data.get("uri") and data.get("shortName") and data.get("type") and data.get("datasetId") and data.get("labelUri")):
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': "Missing parameter"
+ }), 400
+
+ name = data.get("name").strip()
+ uri = data.get("uri").strip()
+ short_name = data.get("shortName")
+ type = data.get("type").strip()
+ dataset_id = data.get("datasetId")
+ label_uri = data.get("labelUri").strip()
+
+ om = OntologyManager(current_app, session)
+
+ if type == "ols" and not om.test_ols_ontology(short_name):
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': "{} ontology not found in OLS".format(short_name)
+ }), 400
+
+ om = OntologyManager(current_app, session)
+ ontologies = om.list_full_ontologies()
+
+ if type not in ["none", "local", "ols"]:
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': "Invalid type"
+ }), 400
+
+ datasets_info = [{'id': dataset_id}]
+
+ try:
+ datasets_handler = DatasetsHandler(current_app, session, datasets_info=datasets_info)
+ datasets_handler.handle_datasets()
+ except IndexError:
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': "Dataset {} not found".format(dataset_id)
+ }), 400
+
+ if not len(datasets_handler.datasets) == 1 or not datasets_handler.datasets[0].public:
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': "Invalid dataset id"
+ }), 400
+
+ dataset = datasets_handler.datasets[0]
+
+ if any([name == onto['name'] or short_name == onto['short_name'] for onto in ontologies]):
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': "Name and short name must be unique"
+ }), 400
+
+ if any([dataset_id == onto['dataset_id'] for onto in ontologies]):
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': "Dataset is already linked to another ontology"
+ }), 400
+
+ try:
+ om.add_ontology(name, uri, short_name, dataset.id, dataset.graph_name, dataset.endpoint, remote_graph=dataset.remote_graph, type=type, label_uri=label_uri)
+ ontologies = om.list_full_ontologies()
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': str(e)
+ }), 500
+
+ return jsonify({
+ 'ontologies': ontologies,
+ 'error': False,
+ 'errorMessage': ''
+ })
+
+
+@admin_bp.route("/api/admin/delete_ontologies", methods=["POST"])
+@api_auth
+@admin_required
+def delete_ontologies():
+ """Delete one or more ontologies
+
+ Returns
+ -------
+ json
+ ontologies: list of all ontologies
+ error: True if error, else False
+ errorMessage: the error message of error, else an empty string
+ """
+
+ data = request.get_json()
+ if not data or not data.get("ontologiesIdToDelete"):
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': "Missing ontologiesIdToDelete parameter"
+ }), 400
+
+ om = OntologyManager(current_app, session)
+
+ ontologies = om.list_full_ontologies()
+ onto_to_delete = [{"id": ontology['id'], "dataset_id": ontology['dataset_id']} for ontology in ontologies if ontology['id'] in data.get("ontologiesIdToDelete")]
+
+ try:
+ om.remove_ontologies(onto_to_delete)
+ ontologies = om.list_full_ontologies()
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ return jsonify({
+ 'ontologies': [],
+ 'error': True,
+ 'errorMessage': str(e)
+ }), 500
+
+ return jsonify({
+ 'ontologies': ontologies,
+ 'error': False,
+ 'errorMessage': ''
+ })
diff --git a/askomics/api/data.py b/askomics/api/data.py
index 2e92d875..24a26cdd 100644
--- a/askomics/api/data.py
+++ b/askomics/api/data.py
@@ -5,7 +5,6 @@
from askomics.api.auth import api_auth
from askomics.libaskomics.SparqlQuery import SparqlQuery
-from askomics.libaskomics.SparqlQueryLauncher import SparqlQueryLauncher
from flask import (Blueprint, current_app, jsonify, session)
@@ -26,7 +25,7 @@ def get_data(uri):
"""
try:
- query = SparqlQuery(current_app, session)
+ query = SparqlQuery(current_app, session, get_graphs=True)
graphs, endpoints = query.get_graphs_and_endpoints(all_selected=True)
endpoints = [val['uri'] for val in endpoints.values()]
@@ -40,14 +39,7 @@ def get_data(uri):
base_uri = current_app.iniconfig.get('triplestore', 'namespace_data')
full_uri = "<%s%s>" % (base_uri, uri)
- raw_query = "SELECT DISTINCT ?predicat ?object\nWHERE {\n?URI ?predicat ?object\nVALUES ?URI {%s}}\n" % (full_uri)
- federated = query.is_federated()
- replace_froms = query.replace_froms()
-
- sparql = query.format_query(raw_query, replace_froms=replace_froms, federated=federated)
-
- query_launcher = SparqlQueryLauncher(current_app, session, get_result_query=True, federated=federated, endpoints=endpoints)
- header, data = query_launcher.process_query(sparql)
+ data = query.get_uri_parameters(full_uri, endpoints)
except Exception as e:
current_app.logger.error(str(e).replace('\\n', '\n'))
diff --git a/askomics/api/datasets.py b/askomics/api/datasets.py
index bceb125e..665a0490 100644
--- a/askomics/api/datasets.py
+++ b/askomics/api/datasets.py
@@ -119,6 +119,7 @@ def toogle_public():
error: True if error, else False
errorMessage: the error message of error, else an empty string
"""
+
data = request.get_json()
if not (data and data.get("id")):
return jsonify({
@@ -135,6 +136,12 @@ def toogle_public():
datasets_handler.handle_datasets()
for dataset in datasets_handler.datasets:
+ if (not data.get("newStatus", False) and dataset.ontology):
+ return jsonify({
+ 'datasets': [],
+ 'error': True,
+ 'errorMessage': "Cannot unpublicize a dataset linked to an ontology"
+ }), 400
current_app.logger.debug(data.get("newStatus", False))
dataset.toggle_public(data.get("newStatus", False))
diff --git a/askomics/api/file.py b/askomics/api/file.py
index 62e9587e..5d224030 100644
--- a/askomics/api/file.py
+++ b/askomics/api/file.py
@@ -1,4 +1,5 @@
"""Api routes"""
+import requests
import sys
import traceback
import urllib
@@ -7,6 +8,7 @@
from askomics.libaskomics.FilesHandler import FilesHandler
from askomics.libaskomics.FilesUtils import FilesUtils
from askomics.libaskomics.Dataset import Dataset
+from askomics.libaskomics.RdfFile import RdfFile
from flask import (Blueprint, current_app, jsonify, request, send_from_directory, session)
@@ -188,8 +190,19 @@ def upload_url():
}), 400
try:
- files = FilesHandler(current_app, session)
- files.download_url(data["url"])
+ if session["user"]["quota"] > 0:
+ with requests.get(data["url"], stream=True) as r:
+ # Check header for total size, and check quota.
+ if r.headers.get('Content-length'):
+ total_size = int(r.headers.get('Content-length')) + disk_space
+ if total_size >= session["user"]["quota"]:
+ return jsonify({
+ 'errorMessage': "File will exceed quota",
+ "error": True
+ }), 400
+
+ session_dict = {'user': session['user']}
+ current_app.celery.send_task('download_file', (session_dict, data["url"]))
except Exception as e:
traceback.print_exc(file=sys.stdout)
return jsonify({
@@ -224,6 +237,7 @@ def get_preview():
}), 400
try:
+
files_handler = FilesHandler(current_app, session)
files_handler.handle_files(data['filesId'])
@@ -318,19 +332,22 @@ def integrate():
for file in files_handler.files:
- data["externalEndpoint"] = data["externalEndpoint"] if data.get("externalEndpoint") else None
- data["customUri"] = data["customUri"] if data.get("customUri") else None
+ data["externalEndpoint"] = data["externalEndpoint"] if (data.get("externalEndpoint") and isinstance(file, RdfFile)) else None
+ data["externalGraph"] = data["externalGraph"] if (data.get("externalGraph") and isinstance(file, RdfFile)) else None
+ data["customUri"] = data["customUri"] if (data.get("customUri") and not isinstance(file, RdfFile)) else None
dataset_info = {
"celery_id": None,
"file_id": file.id,
"name": file.human_name,
"graph_name": file.file_graph,
- "public": data.get("public") if session["user"]["admin"] else False
+ "public": (data.get("public", False) if session["user"]["admin"] else False) or current_app.iniconfig.getboolean("askomics", "single_tenant", fallback=False)
}
+ endpoint = data["externalEndpoint"] or current_app.iniconfig.get('triplestore', 'endpoint')
+
dataset = Dataset(current_app, session, dataset_info)
- dataset.save_in_db()
+ dataset.save_in_db(endpoint, data["externalGraph"])
data["dataset_id"] = dataset.id
dataset_ids.append(dataset.id)
task = current_app.celery.send_task('integrate', (session_dict, data, request.host_url))
@@ -393,7 +410,7 @@ def get_column_types():
types: list of available column types
"""
- data = ["numeric", "text", "category", "boolean", "date", "reference", "strand", "start", "end", "general_relation", "symetric_relation"]
+ data = ["numeric", "text", "category", "boolean", "date", "reference", "strand", "start", "end", "general_relation", "symetric_relation", "label"]
return jsonify({
"types": data
diff --git a/askomics/api/ontology.py b/askomics/api/ontology.py
new file mode 100644
index 00000000..fc1efe37
--- /dev/null
+++ b/askomics/api/ontology.py
@@ -0,0 +1,59 @@
+import traceback
+import sys
+
+from askomics.api.auth import api_auth
+from askomics.libaskomics.OntologyManager import OntologyManager
+
+from flask import (Blueprint, current_app, jsonify, request, session)
+
+onto_bp = Blueprint('ontology', __name__, url_prefix='/')
+
+
+@onto_bp.route("/api/ontology//autocomplete", methods=["GET"])
+@api_auth
+def autocomplete(short_ontology):
+ """Get the default sparql query
+
+ Returns
+ -------
+ json
+ """
+
+ if "user" not in session and current_app.iniconfig.getboolean("askomics", "protect_public"):
+ return jsonify({
+ "error": True,
+ "errorMessage": "Ontology {} not found".format(short_ontology),
+ "results": []
+ }), 401
+ try:
+ om = OntologyManager(current_app, session)
+ ontology = om.get_ontology(short_name=short_ontology)
+ if not ontology:
+ return jsonify({
+ "error": True,
+ "errorMessage": "Ontology {} not found".format(short_ontology),
+ "results": []
+ }), 404
+
+ if ontology['type'] == "none":
+ return jsonify({
+ "error": True,
+ "errorMessage": "Ontology {} does not have autocompletion".format(short_ontology),
+ "results": []
+ }), 404
+
+ results = om.autocomplete(ontology["uri"], ontology["type"], request.args.get("q"), short_ontology, ontology["graph"], ontology["endpoint"], ontology['label_uri'], ontology['remote_graph'])
+
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ return jsonify({
+ "error": True,
+ "errorMessage": str(e),
+ "results": []
+ }), 500
+
+ return jsonify({
+ "error": False,
+ "errorMessage": "",
+ "results": results
+ }), 200
diff --git a/askomics/api/query.py b/askomics/api/query.py
index f5bfc8af..45dd8c6d 100644
--- a/askomics/api/query.py
+++ b/askomics/api/query.py
@@ -186,14 +186,8 @@ def save_result():
'errorMessage': "Missing graphState parameter"
}), 400
- query = SparqlQuery(current_app, session, data["graphState"])
- query.build_query_from_json(preview=False, for_editor=False)
-
info = {
- "graph_state": query.json,
- "query": query.sparql,
- "graphs": query.graphs,
- "endpoints": query.endpoints,
+ "graph_state": data["graphState"],
"celery_id": None
}
diff --git a/askomics/api/results.py b/askomics/api/results.py
index 854aeb08..d87f9e9e 100644
--- a/askomics/api/results.py
+++ b/askomics/api/results.py
@@ -14,6 +14,8 @@
def can_access(user):
+ if not user:
+ return False
login_allowed = current_app.iniconfig.getboolean('askomics', 'enable_sparql_console', fallback=False)
return login_allowed or user['admin']
@@ -164,8 +166,9 @@ def get_graph_and_sparql_query():
graphs = result.graphs
endpoints = result.endpoints
# Get all graphs and endpoint, and mark as selected the used one
- query = SparqlQuery(current_app, session)
+ query = SparqlQuery(current_app, session, get_graphs=True)
graphs, endpoints = query.get_graphs_and_endpoints(selected_graphs=graphs, selected_endpoints=endpoints)
+ console_enabled = can_access(session.get('user'))
except Exception as e:
traceback.print_exc(file=sys.stdout)
@@ -186,7 +189,8 @@ def get_graph_and_sparql_query():
'endpoints': endpoints,
'diskSpace': disk_space,
'error': False,
- 'errorMessage': ''
+ 'errorMessage': '',
+ 'console_enabled': console_enabled
})
@@ -359,7 +363,7 @@ def get_sparql_query():
'error': True,
'errorMessage': "You do not have access to this result"
}), 401
- query = SparqlQuery(current_app, session)
+ query = SparqlQuery(current_app, session, get_graphs=True)
sparql = result.get_sparql_query()
diff --git a/askomics/api/sparql.py b/askomics/api/sparql.py
index c501ab92..043897c8 100644
--- a/askomics/api/sparql.py
+++ b/askomics/api/sparql.py
@@ -32,7 +32,7 @@ def init():
disk_space = files_utils.get_size_occupied_by_user() if "user" in session else None
# Get graphs and endpoints
- query = SparqlQuery(current_app, session)
+ query = SparqlQuery(current_app, session, get_graphs=True)
graphs, endpoints = query.get_graphs_and_endpoints(all_selected=True)
# Default query
diff --git a/askomics/api/start.py b/askomics/api/start.py
index eca8f91b..910af044 100644
--- a/askomics/api/start.py
+++ b/askomics/api/start.py
@@ -5,6 +5,7 @@
from askomics.api.auth import api_auth
from askomics.libaskomics.LocalAuth import LocalAuth
from askomics.libaskomics.Start import Start
+from askomics.libaskomics.OntologyManager import OntologyManager
from flask import (Blueprint, current_app, jsonify, session)
@@ -64,6 +65,9 @@ def start():
except Exception:
pass
+ ontologies_manager = OntologyManager(current_app, session)
+ ontologies = ontologies_manager.list_ontologies()
+
config = {
"footerMessage": current_app.iniconfig.get('askomics', 'footer_message'),
"frontMessage": front_message,
@@ -79,7 +83,10 @@ def start():
"namespaceInternal": current_app.iniconfig.get('triplestore', 'namespace_internal'),
"proxyPath": proxy_path,
"user": {},
- "logged": False
+ "logged": False,
+ "ontologies": ontologies,
+ "singleTenant": current_app.iniconfig.getboolean('askomics', 'single_tenant', fallback=False),
+ "autocompleteMaxResults": current_app.iniconfig.getint("askomics", "autocomplete_max_results", fallback=10)
}
json = {
diff --git a/askomics/app.py b/askomics/app.py
index 1c23650b..e26b858c 100644
--- a/askomics/app.py
+++ b/askomics/app.py
@@ -19,6 +19,7 @@
from askomics.api.view import view_bp
from askomics.api.results import results_bp
from askomics.api.galaxy import galaxy_bp
+from askomics.api.ontology import onto_bp
from celery import Celery
from kombu import Exchange, Queue
@@ -46,7 +47,8 @@
datasets_bp,
query_bp,
results_bp,
- galaxy_bp
+ galaxy_bp,
+ onto_bp
)
diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py
index 7e55a7be..98802ada 100644
--- a/askomics/libaskomics/BedFile.py
+++ b/askomics/libaskomics/BedFile.py
@@ -17,7 +17,7 @@ class BedFile(File):
Public or private dataset
"""
- def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None):
+ def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None, external_graph=None):
"""init
Parameters
@@ -31,7 +31,7 @@ def __init__(self, app, session, file_info, host_url=None, external_endpoint=Non
host_url : None, optional
AskOmics url
"""
- File.__init__(self, app, session, file_info, host_url, external_endpoint=external_endpoint, custom_uri=custom_uri)
+ File.__init__(self, app, session, file_info, host_url, external_endpoint=external_endpoint, custom_uri=custom_uri, external_graph=external_graph)
self.entity_name = ''
self.category_values = {}
@@ -94,13 +94,19 @@ def set_rdf_abstraction_domain_knowledge(self):
self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(self.entity_name, remove_space=True)], rdflib.RDF.type, rdflib.OWL["Class"]))
self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(self.entity_name, remove_space=True)], rdflib.RDFS.label, rdflib.Literal(self.entity_name)))
+ attribute_blanks = {}
+
for attribute in self.attribute_abstraction:
+ blank = BNode()
+
for attr_type in attribute["type"]:
- self.graph_abstraction_dk.add((attribute["uri"], rdflib.RDF.type, attr_type))
- self.graph_abstraction_dk.add((attribute["uri"], rdflib.RDFS.label, attribute["label"]))
- self.graph_abstraction_dk.add((attribute["uri"], rdflib.RDFS.domain, attribute["domain"]))
- self.graph_abstraction_dk.add((attribute["uri"], rdflib.RDFS.range, attribute["range"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, attr_type))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], attribute["uri"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.label, attribute["label"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.domain, attribute["domain"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.range, attribute["range"]))
+ attribute_blanks[attribute["uri"]] = blank
# Domain Knowledge
if "values" in attribute.keys():
for value in attribute["values"]:
@@ -115,7 +121,9 @@ def set_rdf_abstraction_domain_knowledge(self):
if self.faldo_entity:
for key, value in self.faldo_abstraction.items():
if value:
- self.graph_abstraction_dk.add((value, rdflib.RDF.type, self.faldo_abstraction_eq[key]))
+ blank = attribute_blanks[value]
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, self.faldo_abstraction_eq[key]))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], value))
def generate_rdf_content(self):
"""Generate RDF content of the BED file
@@ -218,6 +226,7 @@ def generate_rdf_content(self):
# Strand
strand = False
+ strand_type = None
if feature.strand == "+":
self.category_values["strand"] = {"+", }
relation = self.namespace_data[self.format_uri("strand")]
@@ -226,6 +235,7 @@ def generate_rdf_content(self):
self.faldo_abstraction["strand"] = relation
self.graph_chunk.add((entity, relation, attribute))
strand = True
+ strand_type = "+"
elif feature.strand == "-":
self.category_values["strand"] = {"-", }
relation = self.namespace_data[self.format_uri("strand")]
@@ -234,6 +244,7 @@ def generate_rdf_content(self):
self.faldo_abstraction["strand"] = relation
self.graph_chunk.add((entity, relation, attribute))
strand = True
+ strand_type = "-"
else:
self.category_values["strand"] = {".", }
relation = self.namespace_data[self.format_uri("strand")]
@@ -242,17 +253,18 @@ def generate_rdf_content(self):
self.faldo_abstraction["strand"] = relation
self.graph_chunk.add((entity, relation, attribute))
strand = True
+ strand_type = "."
if strand:
- if "strand" not in attribute_list:
- attribute_list.append("strand")
+ if ("strand", strand_type) not in attribute_list:
+ attribute_list.append(("strand", strand_type))
self.attribute_abstraction.append({
"uri": self.namespace_data[self.format_uri("strand")],
"label": rdflib.Literal("strand"),
"type": [self.namespace_internal[self.format_uri("AskomicsCategory")], rdflib.OWL.ObjectProperty],
"domain": entity_type,
"range": self.namespace_data[self.format_uri("{}Category".format("strand"))],
- "values": ["+", "-", "."]
+ "values": [strand_type]
})
# Score
diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py
index 299a2965..821f00dd 100644
--- a/askomics/libaskomics/CsvFile.py
+++ b/askomics/libaskomics/CsvFile.py
@@ -8,6 +8,7 @@
from rdflib import BNode
from askomics.libaskomics.File import File
+from askomics.libaskomics.OntologyManager import OntologyManager
from askomics.libaskomics.Utils import cached_property
@@ -28,7 +29,7 @@ class CsvFile(File):
Public
"""
- def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None):
+ def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None, external_graph=None):
"""init
Parameters
@@ -42,7 +43,7 @@ def __init__(self, app, session, file_info, host_url=None, external_endpoint=Non
host_url : None, optional
AskOmics url
"""
- File.__init__(self, app, session, file_info, host_url, external_endpoint=external_endpoint, custom_uri=custom_uri)
+ File.__init__(self, app, session, file_info, host_url, external_endpoint=external_endpoint, custom_uri=custom_uri, external_graph=external_graph)
self.preview_limit = 30
try:
self.preview_limit = self.settings.getint("askomics", "npreview")
@@ -191,8 +192,12 @@ def guess_column_type(self, values, header_index):
if self.header[header_index].find("@") > 0:
return "general_relation"
+ # If it matches "label"
+ if header_index == 1 and re.match(r".*label.*", self.header[header_index].lower(), re.IGNORECASE) is not None:
+ return "label"
+
special_types = {
- 'reference': ('chr', 'ref'),
+ 'reference': ('chr', 'ref', 'scaff'),
'strand': ('strand', ),
'start': ('start', 'begin'),
'end': ('end', 'stop'),
@@ -295,7 +300,7 @@ def is_date(value):
try:
parser.parse(value, dayfirst=True).date()
return True
- except parser.ParserError:
+ except Exception:
return False
@property
@@ -357,8 +362,7 @@ def set_rdf_abstraction_domain_knowledge(self):
def set_rdf_domain_knowledge(self):
"""Set the domain knowledge"""
for index, attribute in enumerate(self.header):
-
- if self.columns_type[index] in ('category', 'reference', 'strand'):
+ if self.columns_type[index] in ('category', 'reference', 'strand') and self.header[index] in self.category_values:
s = self.namespace_data["{}Category".format(self.format_uri(attribute, remove_space=True))]
p = self.namespace_internal["category"]
for value in self.category_values[self.header[index]]:
@@ -392,6 +396,11 @@ def set_rdf_abstraction(self):
if self.columns_type[0] == 'start_entity':
self.graph_abstraction_dk.add((entity, rdflib.RDF.type, self.namespace_internal['startPoint']))
+ available_ontologies = {}
+ for ontology in OntologyManager(self.app, self.session).list_ontologies():
+ available_ontologies[ontology['short_name']] = ontology['uri']
+ attribute_blanks = {}
+
# Attributes and relations
for index, attribute_name in enumerate(self.header):
@@ -401,6 +410,11 @@ def set_rdf_abstraction(self):
if index == 0:
continue
+ # Skip label for second column
+ if self.columns_type[index] == "label" and index == 1:
+ continue
+
+ blank = BNode()
# Relation
if self.columns_type[index] in ('general_relation', 'symetric_relation'):
symetric_relation = True if self.columns_type[index] == 'symetric_relation' else False
@@ -410,7 +424,45 @@ def set_rdf_abstraction(self):
label = rdflib.Literal(splitted[0])
rdf_range = self.rdfize(splitted[1])
rdf_type = rdflib.OWL.ObjectProperty
- self.graph_abstraction_dk.add((attribute, rdflib.RDF.type, self.namespace_internal["AskomicsRelation"]))
+
+ # New way of storing relations (starting from 4.4.0)
+
+ endpoint = rdflib.Literal(self.external_endpoint) if self.external_endpoint else rdflib.Literal(self.settings.get('triplestore', 'endpoint'))
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, rdflib.OWL.ObjectProperty))
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, self.namespace_internal["AskomicsRelation"]))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], attribute))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.label, label))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.domain, entity))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.range, rdf_range))
+ self.graph_abstraction_dk.add((blank, rdflib.DCAT.endpointURL, endpoint))
+ self.graph_abstraction_dk.add((blank, rdflib.DCAT.dataset, rdflib.Literal(self.name)))
+ if symetric_relation:
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.domain, rdf_range))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.range, entity))
+
+ continue
+
+ # Manage ontologies
+ if self.columns_type[index] in available_ontologies:
+
+ attribute = self.rdfize(attribute_name)
+ label = rdflib.Literal(attribute_name)
+ rdf_range = self.rdfize(available_ontologies[self.columns_type[index]])
+ rdf_type = rdflib.OWL.ObjectProperty
+
+ # New way of storing relations (starting from 4.4.0)
+ blank = BNode()
+ endpoint = rdflib.Literal(self.external_endpoint) if self.external_endpoint else rdflib.Literal(self.settings.get('triplestore', 'endpoint'))
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, rdflib.OWL.ObjectProperty))
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, self.namespace_internal["AskomicsRelation"]))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], attribute))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.label, label))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.domain, entity))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.range, rdf_range))
+ self.graph_abstraction_dk.add((blank, rdflib.DCAT.endpointURL, endpoint))
+ self.graph_abstraction_dk.add((blank, rdflib.DCAT.dataset, rdflib.Literal(self.name)))
+
+ continue
# Category
elif self.columns_type[index] in ('category', 'reference', 'strand'):
@@ -418,7 +470,7 @@ def set_rdf_abstraction(self):
label = rdflib.Literal(attribute_name)
rdf_range = self.namespace_data["{}Category".format(self.format_uri(attribute_name, remove_space=True))]
rdf_type = rdflib.OWL.ObjectProperty
- self.graph_abstraction_dk.add((attribute, rdflib.RDF.type, self.namespace_internal["AskomicsCategory"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, self.namespace_internal["AskomicsCategory"]))
# Numeric
elif self.columns_type[index] in ('numeric', 'start', 'end'):
@@ -448,19 +500,22 @@ def set_rdf_abstraction(self):
rdf_range = rdflib.XSD.string
rdf_type = rdflib.OWL.DatatypeProperty
- self.graph_abstraction_dk.add((attribute, rdflib.RDF.type, rdf_type))
- self.graph_abstraction_dk.add((attribute, rdflib.RDFS.label, label))
- self.graph_abstraction_dk.add((attribute, rdflib.RDFS.domain, entity))
- self.graph_abstraction_dk.add((attribute, rdflib.RDFS.range, rdf_range))
- if symetric_relation:
- self.graph_abstraction_dk.add((attribute, rdflib.RDFS.domain, rdf_range))
- self.graph_abstraction_dk.add((attribute, rdflib.RDFS.range, entity))
+ attribute_blanks[attribute] = blank
+
+ # New way of storing attributes (starting from 4.4.0)
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, rdf_type))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], attribute))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.label, label))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.domain, entity))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.range, rdf_range))
# Faldo:
if self.faldo_entity:
for key, value in self.faldo_abstraction.items():
if value:
- self.graph_abstraction_dk.add((value, rdflib.RDF.type, self.faldo_abstraction_eq[key]))
+ blank = attribute_blanks[value]
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, self.faldo_abstraction_eq[key]))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], value))
def generate_rdf_content(self):
"""Generator of the rdf content
@@ -472,6 +527,10 @@ def generate_rdf_content(self):
"""
total_lines = sum(1 for line in open(self.path))
+ available_ontologies = {}
+ for ontology in OntologyManager(self.app, self.session).list_ontologies():
+ available_ontologies[ontology['short_name']] = ontology['uri']
+
with open(self.path, 'r', encoding='utf-8') as file:
reader = csv.reader(file, dialect=self.dialect)
@@ -489,6 +548,11 @@ def generate_rdf_content(self):
# Faldo
self.faldo_entity = True if 'start' in self.columns_type and 'end' in self.columns_type else False
+ has_label = None
+ # Get first value, ignore others
+ if "label" in self.columns_type and self.columns_type.index("label") == 1:
+ has_label = True
+
# Loop on lines
for row_number, row in enumerate(reader):
@@ -501,7 +565,10 @@ def generate_rdf_content(self):
# Entity
entity = self.rdfize(row[0], custom_namespace=self.namespace_entity)
- label = self.get_uri_label(row[0])
+ if has_label and row[1]:
+ label = row[1]
+ else:
+ label = self.get_uri_label(row[0])
self.graph_chunk.add((entity, rdflib.RDF.type, entity_type))
self.graph_chunk.add((entity, rdflib.RDFS.label, rdflib.Literal(label)))
@@ -525,6 +592,11 @@ def generate_rdf_content(self):
relation = None
symetric_relation = False
+ # Skip label type for second column
+ # if type is label but not second column, default to string
+ if current_type == "label" and column_number == 1:
+ continue
+
# Skip entity and blank cells
if column_number == 0 or (not cell and not current_type == "strand"):
continue
@@ -536,6 +608,12 @@ def generate_rdf_content(self):
relation = self.rdfize(splitted[0])
attribute = self.rdfize(cell)
+ # Ontology
+ elif current_type in available_ontologies:
+ symetric_relation = False
+ relation = self.rdfize(current_header)
+ attribute = self.rdfize(cell)
+
# Category
elif current_type in ('category', 'reference', 'strand'):
potential_relation = self.rdfize(current_header)
@@ -583,7 +661,7 @@ def generate_rdf_content(self):
elif current_type == "date":
relation = self.rdfize(current_header)
- attribute = rdflib.Literal(self.convert_type(cell))
+ attribute = rdflib.Literal(self.convert_type(cell, try_date=True))
# default is text
else:
diff --git a/askomics/libaskomics/Database.py b/askomics/libaskomics/Database.py
index 5afadeae..9fc02acb 100644
--- a/askomics/libaskomics/Database.py
+++ b/askomics/libaskomics/Database.py
@@ -76,6 +76,8 @@ def init_database(self):
self.create_files_table()
self.create_datasets_table()
self.create_abstraction_table()
+ self.create_prefixes_table()
+ self.create_ontologies_table()
def create_user_table(self):
"""Create the user table"""
@@ -196,6 +198,39 @@ def update_datasets_table(self):
except Exception:
pass
+ query = '''
+ ALTER TABLE datasets
+ ADD ontology boolean NULL
+ DEFAULT(0)
+ '''
+
+ try:
+ self.execute_sql_query(query)
+ except Exception:
+ pass
+
+ query = '''
+ ALTER TABLE datasets
+ ADD endpoint text NULL
+ DEFAULT(null)
+ '''
+
+ try:
+ self.execute_sql_query(query)
+ except Exception:
+ pass
+
+ query = '''
+ ALTER TABLE datasets
+ ADD remote_graph text NULL
+ DEFAULT(null)
+ '''
+
+ try:
+ self.execute_sql_query(query)
+ except Exception:
+ pass
+
def create_integration_table(self):
"""Create the integration table"""
query = '''
@@ -350,6 +385,28 @@ def create_files_table(self):
'''
self.execute_sql_query(query)
+ query = '''
+ ALTER TABLE files
+ ADD status text NOT NULL
+ DEFAULT('available')
+ '''
+
+ try:
+ self.execute_sql_query(query)
+ except Exception:
+ pass
+
+ query = '''
+ ALTER TABLE files
+ ADD task_id text NULL
+ DEFAULT(NULL)
+ '''
+
+ try:
+ self.execute_sql_query(query)
+ except Exception:
+ pass
+
def create_abstraction_table(self):
"""Create abstraction table"""
query = """
@@ -361,3 +418,61 @@ def create_abstraction_table(self):
)
"""
self.execute_sql_query(query)
+
+ def create_prefixes_table(self):
+ """Create the prefix table"""
+ query = '''
+ CREATE TABLE IF NOT EXISTS prefixes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ prefix text NOT NULL,
+ namespace text NOT NULL
+ )
+ '''
+ self.execute_sql_query(query)
+
+ def create_ontologies_table(self):
+ """Create the ontologies table"""
+ query = '''
+ CREATE TABLE IF NOT EXISTS ontologies (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name text NOT NULL,
+ uri text NOT NULL,
+ short_name text NOT NULL,
+ type text DEFAULT 'sparql',
+ dataset_id INTEGER NOT NULL,
+ graph text NOT NULL,
+ FOREIGN KEY(dataset_id) REFERENCES datasets(id)
+ )
+ '''
+ self.execute_sql_query(query)
+
+ query = '''
+ ALTER TABLE ontologies
+ ADD label_uri text NOT NULL
+ DEFAULT('rdfs:label')
+ '''
+
+ try:
+ self.execute_sql_query(query)
+ except Exception:
+ pass
+
+ query = '''
+ ALTER TABLE ontologies
+ ADD endpoint text NULL
+ '''
+
+ try:
+ self.execute_sql_query(query)
+ except Exception:
+ pass
+
+ query = '''
+ ALTER TABLE ontologies
+ ADD remote_graph text NULL
+ '''
+
+ try:
+ self.execute_sql_query(query)
+ except Exception:
+ pass
diff --git a/askomics/libaskomics/Dataset.py b/askomics/libaskomics/Dataset.py
index 927f4896..c3fd7b1b 100644
--- a/askomics/libaskomics/Dataset.py
+++ b/askomics/libaskomics/Dataset.py
@@ -45,6 +45,9 @@ def __init__(self, app, session, dataset_info={}):
self.public = dataset_info["public"] if "public" in dataset_info else False
self.start = dataset_info["start"] if "start" in dataset_info else None
self.end = dataset_info["end"] if "end" in dataset_info else None
+ self.ontology = dataset_info["ontology"] if "ontology" in dataset_info else False
+ self.endpoint = dataset_info["endpoint"] if "endpoint" in dataset_info else False
+ self.remote_graph = dataset_info["remote_graph"] if "remote_graph" in dataset_info else False
def set_info_from_db(self, admin=False):
"""Set the info in from the database"""
@@ -58,7 +61,7 @@ def set_info_from_db(self, admin=False):
where_query = "AND user_id = ?"
query = '''
- SELECT celery_id, file_id, name, graph_name, public, start, end
+ SELECT celery_id, file_id, name, graph_name, public, start, end, ontology, endpoint, remote_graph
FROM datasets
WHERE id = ?
{}
@@ -73,11 +76,40 @@ def set_info_from_db(self, admin=False):
self.public = rows[0][4]
self.start = rows[0][5]
self.end = rows[0][6]
+ self.ontology = rows[0][7]
+ self.endpoint = rows[0][8]
+ self.remote_graph = rows[0][9]
- def save_in_db(self):
+ def save_in_db(self, endpoint, remote_graph=None, set_graph=False):
"""Save the dataset into the database"""
database = Database(self.app, self.session)
+ subquery = "NULL"
+ args = (
+ self.session["user"]["id"],
+ self.celery_id,
+ self.file_id,
+ self.name,
+ self.public,
+ 0,
+ endpoint,
+ remote_graph
+ )
+
+ if set_graph:
+ subquery = "?"
+ args = (
+ self.session["user"]["id"],
+ self.celery_id,
+ self.file_id,
+ self.name,
+ self.graph_name,
+ self.public,
+ 0,
+ endpoint,
+ remote_graph
+ )
+
query = '''
INSERT INTO datasets VALUES(
NULL,
@@ -85,7 +117,7 @@ def save_in_db(self):
?,
?,
?,
- NULL,
+ {},
?,
"queued",
strftime('%s', 'now'),
@@ -93,18 +125,14 @@ def save_in_db(self):
?,
NULL,
NULL,
- NULL
+ NULL,
+ 0,
+ ?,
+ ?
)
- '''
+ '''.format(subquery)
- self.id = database.execute_sql_query(query, (
- self.session["user"]["id"],
- self.celery_id,
- self.file_id,
- self.name,
- self.public,
- 0
- ), get_id=True)
+ self.id = database.execute_sql_query(query, args, get_id=True)
def toggle_public(self, new_status, admin=False):
"""Change public status of a dataset (triplestore and db)
@@ -228,3 +256,10 @@ def delete_from_db(self, admin=False):
'''.format(where_query)
database.execute_sql_query(query, query_params)
+
+ query = '''
+ DELETE FROM ontologies
+ WHERE dataset_id = ?
+ '''
+
+ database.execute_sql_query(query, (self.id,))
diff --git a/askomics/libaskomics/DatasetsHandler.py b/askomics/libaskomics/DatasetsHandler.py
index 332dfc80..cdd1d172 100644
--- a/askomics/libaskomics/DatasetsHandler.py
+++ b/askomics/libaskomics/DatasetsHandler.py
@@ -51,7 +51,7 @@ def get_datasets(self):
database = Database(self.app, self.session)
query = '''
- SELECT id, name, public, status, start, end, ntriples, error_message, traceback, percent
+ SELECT id, name, public, status, start, end, ntriples, error_message, traceback, percent, ontology
FROM datasets
WHERE user_id = ?
'''
@@ -76,7 +76,8 @@ def get_datasets(self):
'ntriples': row[6],
'error_message': row[7],
'traceback': row[8],
- 'percent': row[9]
+ 'percent': row[9],
+ 'ontology': row[10]
}
datasets.append(dataset)
@@ -97,7 +98,7 @@ def get_all_datasets(self):
database = Database(self.app, self.session)
query = '''
- SELECT datasets.id, datasets.name, datasets.public, datasets.status, datasets.start, datasets.end, datasets.ntriples, datasets.error_message, datasets.traceback, datasets.percent, users.username
+ SELECT datasets.id, datasets.name, datasets.public, datasets.status, datasets.start, datasets.end, datasets.ntriples, datasets.error_message, datasets.traceback, datasets.percent, users.username, datasets.ontology
FROM datasets
INNER JOIN users ON datasets.user_id=users.user_id
'''
@@ -123,7 +124,8 @@ def get_all_datasets(self):
'error_message': row[7],
'traceback': row[8],
'percent': row[9],
- 'user': row[10]
+ 'user': row[10],
+ 'ontology': row[11]
}
datasets.append(dataset)
diff --git a/askomics/libaskomics/File.py b/askomics/libaskomics/File.py
index 24292fa8..030f30e3 100644
--- a/askomics/libaskomics/File.py
+++ b/askomics/libaskomics/File.py
@@ -69,7 +69,7 @@ class File(Params):
User graph
"""
- def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None):
+ def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None, external_graph=None):
"""init
Parameters
@@ -97,6 +97,7 @@ def __init__(self, app, session, file_info, host_url=None, external_endpoint=Non
self.ntriples = 0
self.timestamp = int(time.time())
self.external_endpoint = external_endpoint
+ self.external_graph = external_graph
self.default_graph = "{}".format(self.settings.get('triplestore', 'default_graph'))
self.user_graph = "{}:{}_{}".format(
@@ -130,6 +131,7 @@ def __init__(self, app, session, file_info, host_url=None, external_endpoint=Non
self.faldo = Namespace('http://biohackathon.org/resource/faldo/')
self.prov = Namespace('http://www.w3.org/ns/prov#')
self.dc = Namespace('http://purl.org/dc/elements/1.1/')
+ self.dcat = Namespace('http://www.w3.org/ns/dcat#')
self.faldo_entity = False
self.faldo_abstraction = {
@@ -277,6 +279,8 @@ def set_metadata(self):
self.graph_metadata.add((rdflib.Literal(self.file_graph), self.prov.wasDerivedFrom, rdflib.Literal(self.name)))
self.graph_metadata.add((rdflib.Literal(self.file_graph), self.dc.hasVersion, rdflib.Literal(get_distribution('askomics').version)))
self.graph_metadata.add((rdflib.Literal(self.file_graph), self.prov.describesService, rdflib.Literal(os.uname()[1])))
+ if self.external_graph:
+ self.graph_metadata.add((rdflib.Literal(self.file_graph), self.dcat.Dataset, rdflib.Literal(self.external_graph)))
if self.public:
self.graph_metadata.add((rdflib.Literal(self.file_graph), self.namespace_internal['public'], rdflib.Literal(True)))
@@ -461,7 +465,7 @@ def get_rdf_type(self, value):
return rdflib.XSD.string
- def convert_type(self, value):
+ def convert_type(self, value, try_date=False):
"""Convert a value to a date, an int or float or text
Parameters
@@ -480,9 +484,11 @@ def convert_type(self, value):
try:
return float(value)
except ValueError:
+ if not try_date:
+ return value
try:
return parser.parse(value, dayfirst=True).date()
- except parser.ParserError:
+ except Exception:
return value
return value
diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py
index 54bc495c..04254028 100644
--- a/askomics/libaskomics/FilesHandler.py
+++ b/askomics/libaskomics/FilesHandler.py
@@ -24,7 +24,7 @@ class FilesHandler(FilesUtils):
Upload path
"""
- def __init__(self, app, session, host_url=None, external_endpoint=None, custom_uri=None):
+ def __init__(self, app, session, host_url=None, external_endpoint=None, custom_uri=None, external_graph=None):
"""init
Parameters
@@ -47,6 +47,7 @@ def __init__(self, app, session, host_url=None, external_endpoint=None, custom_u
self.date = None
self.external_endpoint = external_endpoint
self.custom_uri = custom_uri
+ self.external_graph = external_graph
def handle_files(self, files_id):
"""Handle file
@@ -60,13 +61,13 @@ def handle_files(self, files_id):
for file in files_infos:
if file['type'] == 'csv/tsv':
- self.files.append(CsvFile(self.app, self.session, file, host_url=self.host_url, external_endpoint=self.external_endpoint, custom_uri=self.custom_uri))
+ self.files.append(CsvFile(self.app, self.session, file, host_url=self.host_url, external_endpoint=self.external_endpoint, custom_uri=self.custom_uri, external_graph=self.external_graph))
elif file['type'] == 'gff/gff3':
- self.files.append(GffFile(self.app, self.session, file, host_url=self.host_url, external_endpoint=self.external_endpoint, custom_uri=self.custom_uri))
+ self.files.append(GffFile(self.app, self.session, file, host_url=self.host_url, external_endpoint=self.external_endpoint, custom_uri=self.custom_uri, external_graph=self.external_graph))
elif file['type'] in ('rdf/ttl', 'rdf/xml', 'rdf/nt'):
- self.files.append(RdfFile(self.app, self.session, file, host_url=self.host_url, external_endpoint=self.external_endpoint, custom_uri=self.custom_uri))
+ self.files.append(RdfFile(self.app, self.session, file, host_url=self.host_url, external_endpoint=self.external_endpoint, custom_uri=self.custom_uri, external_graph=self.external_graph))
elif file['type'] == 'bed':
- self.files.append(BedFile(self.app, self.session, file, host_url=self.host_url, external_endpoint=self.external_endpoint, custom_uri=self.custom_uri))
+ self.files.append(BedFile(self.app, self.session, file, host_url=self.host_url, external_endpoint=self.external_endpoint, custom_uri=self.custom_uri, external_graph=self.external_graph))
def get_files_infos(self, files_id=None, files_path=None, return_path=False):
"""Get files info
@@ -89,7 +90,7 @@ def get_files_infos(self, files_id=None, files_path=None, return_path=False):
subquery_str = '(' + ' OR '.join(['id = ?'] * len(files_id)) + ')'
query = '''
- SELECT id, name, type, size, path, date
+ SELECT id, name, type, size, path, date, status
FROM files
WHERE user_id = ?
AND {}
@@ -101,7 +102,7 @@ def get_files_infos(self, files_id=None, files_path=None, return_path=False):
subquery_str = '(' + ' OR '.join(['path = ?'] * len(files_path)) + ')'
query = '''
- SELECT id, name, type, size, path, date
+ SELECT id, name, type, size, path, date, status
FROM files
WHERE user_id = ?
AND {}
@@ -112,7 +113,7 @@ def get_files_infos(self, files_id=None, files_path=None, return_path=False):
else:
query = '''
- SELECT id, name, type, size, path, date
+ SELECT id, name, type, size, path, date, status
FROM files
WHERE user_id = ?
'''
@@ -126,7 +127,8 @@ def get_files_infos(self, files_id=None, files_path=None, return_path=False):
'name': row[1],
'type': row[2],
'size': row[3],
- 'date': row[5]
+ 'date': row[5],
+ 'status': row[6]
}
if return_path:
file['path'] = row[4]
@@ -142,7 +144,7 @@ def get_all_files_infos(self):
database = Database(self.app, self.session)
query = '''
- SELECT files.id, files.name, files.type, files.size, files.date, users.username
+ SELECT files.id, files.name, files.type, files.size, files.date, files.status, users.username
FROM files
INNER JOIN users ON files.user_id=users.user_id
'''
@@ -157,7 +159,8 @@ def get_all_files_infos(self):
'type': row[2],
'size': row[3],
'date': row[4],
- 'user': row[5]
+ 'status': row[5],
+ 'user': row[6]
}
files.append(file)
@@ -203,7 +206,7 @@ def write_data_into_file(self, data, file_name, mode, should_exist=False):
with open(file_path, mode) as file:
file.write(data)
- def store_file_info_in_db(self, name, filetype, file_name, size):
+ def store_file_info_in_db(self, name, filetype, file_name, size, status="available", task_id=None):
"""Store the file info in the database
Parameters
@@ -216,6 +219,12 @@ def store_file_info_in_db(self, name, filetype, file_name, size):
Local file name
size : string
Size of file
+ status: string
+ Status of the file (downloading, available, unavailable)
+ Returns
+ -------
+ str
+ file id
"""
file_path = "{}/{}".format(self.upload_path, file_name)
@@ -228,6 +237,8 @@ def store_file_info_in_db(self, name, filetype, file_name, size):
?,
?,
?,
+ ?,
+ ?,
?
)
'''
@@ -237,7 +248,7 @@ def store_file_info_in_db(self, name, filetype, file_name, size):
filetype = 'csv/tsv'
elif filetype in ('text/turtle', 'ttl'):
filetype = 'rdf/ttl'
- elif filetype == "text/xml":
+ elif filetype in ["text/xml", "application/rdf+xml"]:
filetype = "rdf/xml"
elif filetype == "application/n-triples":
filetype = "rdf/nt"
@@ -248,7 +259,57 @@ def store_file_info_in_db(self, name, filetype, file_name, size):
self.date = int(time.time())
- database.execute_sql_query(query, (self.session['user']['id'], name, filetype, file_path, size, self.date))
+ return database.execute_sql_query(query, (self.session['user']['id'], name, filetype, file_path, size, self.date, status, task_id), get_id=True)
+
+ def update_file_info(self, file_id, size=None, status="", task_id=""):
+ """Update file size and status
+
+ Parameters
+ ----------
+ file_id : str
+ File id
+ file_size : str
+ File current size
+ status : str
+ File status
+ task_id : str
+ Current task id
+ """
+
+ if not (size is not None or status or task_id):
+ return
+
+ query_vars = []
+ database = Database(self.app, self.session)
+
+ size_query = ""
+ status_query = ""
+ task_query = ""
+
+ # Should be a cleaner way of doing this...
+ if size is not None:
+ size_query = "size=?," if (status or task_id) else "size=?"
+ query_vars.append(size)
+
+ if status:
+ status_query = "status=?," if task_id else "status=?"
+ query_vars.append(status)
+
+ if task_id:
+ task_query = "task_id=?"
+ query_vars.append(task_id)
+
+ query_vars.append(file_id)
+
+ query = '''
+ UPDATE files SET
+ {}
+ {}
+ {}
+ WHERE id=?
+ '''.format(size_query, status_query, task_query)
+
+ database.execute_sql_query(query, tuple(query_vars))
def persist_chunk(self, chunk_info):
"""Persist a file by chunk. Store info in db if the chunk is the last
@@ -297,7 +358,7 @@ def persist_chunk(self, chunk_info):
pass
raise(e)
- def download_url(self, url):
+ def download_url(self, url, task_id):
"""Download a file from an URL and insert info in database
Parameters
@@ -309,14 +370,33 @@ def download_url(self, url):
name = url.split("/")[-1]
file_name = self.get_file_name()
path = "{}/{}".format(self.upload_path, file_name)
+ file_id = self.store_file_info_in_db(name, "", file_name, 0, "downloading", task_id)
# Get file
- req = requests.get(url)
- with open(path, 'wb') as file:
- file.write(req.content)
-
- # insert in db
- self.store_file_info_in_db(name, "", file_name, os.path.getsize(path))
+ try:
+ with requests.get(url, stream=True) as r:
+ r.raise_for_status()
+ count = 0
+ with open(path, 'wb') as file:
+ for chunk in r.iter_content(chunk_size=1024 * 1024 * 10):
+ # Update size every ~1GO
+ # + Check quota
+ if count == 100:
+ if self.session['user']['quota'] > 0:
+ total_size = self.get_size_occupied_by_user() + os.path.getsize(path)
+ if total_size >= self.session['user']['quota']:
+ raise Exception("Exceeded quota")
+ self.update_file_info(file_id, size=os.path.getsize(path))
+ count = 0
+
+ file.write(chunk)
+ count += 1
+
+ # Update final value
+ self.update_file_info(file_id, size=os.path.getsize(path), status="available")
+
+ except Exception:
+ self.update_file_info(file_id, size=os.path.getsize(path), status="error")
def get_type(self, file_ext):
"""Get files type, based on extension
diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py
index 102691b4..7c413de7 100644
--- a/askomics/libaskomics/GffFile.py
+++ b/askomics/libaskomics/GffFile.py
@@ -18,7 +18,7 @@ class GffFile(File):
Public or private dataset
"""
- def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None):
+ def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None, external_graph=None):
"""init
Parameters
@@ -32,7 +32,7 @@ def __init__(self, app, session, file_info, host_url=None, external_endpoint=Non
host_url : None, optional
AskOmics url
"""
- File.__init__(self, app, session, file_info, host_url, external_endpoint=external_endpoint, custom_uri=custom_uri)
+ File.__init__(self, app, session, file_info, host_url, external_endpoint=external_endpoint, custom_uri=custom_uri, external_graph=external_graph)
self.entities = []
self.entities_to_integrate = []
@@ -108,13 +108,32 @@ def set_rdf_abstraction_domain_knowledge(self):
self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(entity, remove_space=True)], rdflib.RDF.type, rdflib.OWL["Class"]))
self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(entity, remove_space=True)], rdflib.RDFS.label, rdflib.Literal(entity)))
- for attribute in self.attribute_abstraction:
- for attr_type in attribute["type"]:
- self.graph_abstraction_dk.add((attribute["uri"], rdflib.RDF.type, attr_type))
- self.graph_abstraction_dk.add((attribute["uri"], rdflib.RDFS.label, attribute["label"]))
- self.graph_abstraction_dk.add((attribute["uri"], rdflib.RDFS.domain, attribute["domain"]))
- self.graph_abstraction_dk.add((attribute["uri"], rdflib.RDFS.range, attribute["range"]))
+ attribute_blanks = {}
+ for attribute in self.attribute_abstraction:
+ blank = BNode()
+ # New way of storing relations (starting from 4.4.0)
+ if attribute.get("relation"):
+ endpoint = rdflib.Literal(self.external_endpoint) if self.external_endpoint else rdflib.Literal(self.settings.get('triplestore', 'endpoint'))
+ for attr_type in attribute["type"]:
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, attr_type))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], attribute["uri"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.label, attribute["label"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.domain, attribute["domain"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.range, attribute["range"]))
+ self.graph_abstraction_dk.add((blank, rdflib.DCAT.endpointURL, endpoint))
+ self.graph_abstraction_dk.add((blank, rdflib.DCAT.dataset, rdflib.Literal(self.name)))
+
+ else:
+ # New way of storing attributes (starting from 4.4.0)
+ for attr_type in attribute["type"]:
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, attr_type))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], attribute["uri"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.label, attribute["label"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.domain, attribute["domain"]))
+ self.graph_abstraction_dk.add((blank, rdflib.RDFS.range, attribute["range"]))
+
+ attribute_blanks[attribute["uri"]] = blank
# Domain Knowledge
if "values" in attribute.keys():
for value in attribute["values"]:
@@ -129,7 +148,9 @@ def set_rdf_abstraction_domain_knowledge(self):
if self.faldo_entity:
for key, value in self.faldo_abstraction.items():
if value:
- self.graph_abstraction_dk.add((value, rdflib.RDF.type, self.faldo_abstraction_eq[key]))
+ blank = attribute_blanks[value]
+ self.graph_abstraction_dk.add((blank, rdflib.RDF.type, self.faldo_abstraction_eq[key]))
+ self.graph_abstraction_dk.add((blank, self.namespace_internal["uri"], value))
def format_gff_entity(self, entity):
"""Format a gff entity name by removing type (type:entity --> entity)
@@ -182,7 +203,7 @@ def generate_rdf_content(self):
faldo_strand = None
faldo_start = None
faldo_end = None
-
+ strand_type = None
# Entity
if not feature.id:
if "ID" not in feature.qualifiers.keys():
@@ -272,6 +293,7 @@ def generate_rdf_content(self):
attribute = self.namespace_data[self.format_uri("+")]
faldo_strand = self.get_faldo_strand("+")
self.faldo_abstraction["strand"] = relation
+ strand_type = "+"
# self.graph_chunk.add((entity, relation, attribute))
elif feature.location.strand == -1:
self.category_values["strand"] = {"-", }
@@ -279,6 +301,7 @@ def generate_rdf_content(self):
attribute = self.namespace_data[self.format_uri("-")]
faldo_strand = self.get_faldo_strand("-")
self.faldo_abstraction["strand"] = relation
+ strand_type = "-"
# self.graph_chunk.add((entity, relation, attribute))
else:
self.category_values["strand"] = {".", }
@@ -286,16 +309,17 @@ def generate_rdf_content(self):
attribute = self.namespace_data[self.format_uri(".")]
faldo_strand = self.get_faldo_strand(".")
self.faldo_abstraction["strand"] = relation
+ strand_type = "."
- if (feature.type, "strand") not in attribute_list:
- attribute_list.append((feature.type, "strand"))
+ if (feature.type, "strand", strand_type) not in attribute_list:
+ attribute_list.append((feature.type, "strand", strand_type))
self.attribute_abstraction.append({
"uri": self.namespace_data[self.format_uri("strand")],
"label": rdflib.Literal("strand"),
"type": [self.namespace_internal[self.format_uri("AskomicsCategory")], rdflib.OWL.ObjectProperty],
"domain": entity_type,
"range": self.namespace_data[self.format_uri("{}Category".format("strand"))],
- "values": ["+", "-", "."]
+ "values": [strand_type]
})
# Qualifiers (9th columns)
@@ -319,7 +343,8 @@ def generate_rdf_content(self):
"domain": entity_type,
"range": value,
"qualifier_key": qualifier_key,
- "feature_type": feature.type
+ "feature_type": feature.type,
+ "relation": True
})
skip = True
else:
@@ -336,7 +361,8 @@ def generate_rdf_content(self):
"label": rdflib.Literal(qualifier_key),
"type": [rdflib.OWL.ObjectProperty, self.namespace_internal[self.format_uri("AskomicsRelation")]],
"domain": entity_type,
- "range": self.namespace_data[self.format_uri(related_type)]
+ "range": self.namespace_data[self.format_uri(related_type)],
+ "relation": True
})
else:
diff --git a/askomics/libaskomics/LocalAuth.py b/askomics/libaskomics/LocalAuth.py
index 4759cedb..a1bf0c4f 100644
--- a/askomics/libaskomics/LocalAuth.py
+++ b/askomics/libaskomics/LocalAuth.py
@@ -1044,22 +1044,21 @@ def send_mail_to_new_user(self, user):
mailer = Mailer(self.app, self.session)
if mailer.check_mailer():
body = textwrap.dedent("""
- Welcome {username}!
+ Dear {username}!
- We heard that administrators of {url} create an account for you.
+ An account with this email adress was created by the administrators of {url}.
- To use it, use the following link to create your password. Then, login with you email adress ({email}) or username ({username}).
+ To use it, please use the following link to create your password. You will then be able to log in with your username ({username}).
{url}/password_reset?token={token}
- If you don’t use this link within 3 hours, it will expire. To get a new password creation link, visit {url}/password_reset
+ This link will expire after 3 hours. To get a new password creation link, please visit {url}/password_reset
Thanks,
The AskOmics Team
""".format(
username=user["username"],
- email=user["email"],
url=self.settings.get('askomics', 'instance_url'),
token=token
))
@@ -1104,16 +1103,17 @@ def send_reset_link(self, login):
body = textwrap.dedent("""
Dear {user},
- We heard that you lost your AskOmics password. Sorry about that!
+ A password reset request has been received for your {url} account.
- But don’t worry! You can use the following link to reset your password:
+ If you did not initiate this request, feel free to ignore this message.
- {url}/password_reset?token={token}
+ You can use the following link to reset your password:
- If you don’t use this link within 3 hours, it will expire. To get a new password reset link, visit {url}/password_reset
+ {url}/password_reset?token={token}
+ This link will expire after 3 hours. To get a new password reset link, please visit {url}/password_reset
- Thanks,
+ Best regards,
The AskOmics Team
""".format(
diff --git a/askomics/libaskomics/OntologyManager.py b/askomics/libaskomics/OntologyManager.py
new file mode 100644
index 00000000..eed16a87
--- /dev/null
+++ b/askomics/libaskomics/OntologyManager.py
@@ -0,0 +1,279 @@
+import requests
+
+from collections import defaultdict
+from urllib.parse import quote_plus
+
+
+from askomics.libaskomics.Database import Database
+from askomics.libaskomics.Params import Params
+
+
+class OntologyManager(Params):
+ """Manage ontologies
+
+ Attributes
+ ----------
+ namespace_internal : str
+ askomics namespace, from config file
+ namespace_data : str
+ askomics prefix, from config file
+ prefix : dict
+ dict of all prefixes
+ """
+
+ def __init__(self, app, session):
+ """init
+
+ Parameters
+ ----------
+ app : Flask
+ Flask app
+ session :
+ AskOmics session
+ """
+ Params.__init__(self, app, session)
+
+ def list_ontologies(self):
+ """Get all ontologies
+
+ Returns
+ -------
+ list
+ ontologies
+ """
+
+ database = Database(self.app, self.session)
+
+ query = '''
+ SELECT id, name, uri, short_name, type
+ FROM ontologies
+ '''
+
+ rows = database.execute_sql_query(query)
+
+ ontologies = []
+ for row in rows:
+ prefix = {
+ 'id': row[0],
+ 'name': row[1],
+ 'uri': row[2],
+ 'short_name': row[3],
+ 'type': row[4]
+ }
+ ontologies.append(prefix)
+
+ return ontologies
+
+ def list_full_ontologies(self):
+ """Get all ontologies for admin
+
+ Returns
+ -------
+ list
+ ontologies
+ """
+
+ database = Database(self.app, self.session)
+
+ query = '''
+ SELECT ontologies.id, ontologies.name, ontologies.uri, ontologies.short_name, ontologies.type, ontologies.label_uri, datasets.id, datasets.name, ontologies.graph, ontologies.endpoint, ontologies.remote_graph
+ FROM ontologies
+ INNER JOIN datasets ON datasets.id=ontologies.dataset_id
+ '''
+
+ rows = database.execute_sql_query(query)
+
+ ontologies = []
+ for row in rows:
+ prefix = {
+ 'id': row[0],
+ 'name': row[1],
+ 'uri': row[2],
+ 'short_name': row[3],
+ 'type': row[4],
+ 'label_uri': row[5],
+ 'dataset_id': row[6],
+ 'dataset_name': row[7],
+ 'graph': row[8],
+ 'endpoint': row[9],
+ 'remote_graph': row[10]
+ }
+ ontologies.append(prefix)
+
+ return ontologies
+
+ def get_ontology(self, short_name="", uri=""):
+ """Get a specific ontology based on short name or uri
+
+ Returns
+ -------
+ dict
+ ontology
+ """
+
+ if not (short_name or uri):
+ return None
+
+ if short_name:
+ where_clause = "WHERE short_name = ?"
+ args = (short_name,)
+
+ if uri:
+ where_clause = "WHERE uri = ?"
+ args = (uri,)
+
+ database = Database(self.app, self.session)
+
+ query = '''
+ SELECT id, name, uri, short_name, type, dataset_id, graph, label_uri, endpoint, remote_graph
+ FROM ontologies
+ {}
+ '''.format(where_clause)
+
+ rows = database.execute_sql_query(query, args)
+
+ if not rows:
+ return None
+
+ ontology = rows[0]
+ return {
+ 'id': ontology[0],
+ 'name': ontology[1],
+ 'uri': ontology[2],
+ 'short_name': ontology[3],
+ 'type': ontology[4],
+ 'dataset_id': ontology[5],
+ 'graph': ontology[6],
+ 'label_uri': ontology[7],
+ 'endpoint': ontology[8],
+ 'remote_graph': ontology[9]
+ }
+
+ def add_ontology(self, name, uri, short_name, dataset_id, graph, endpoint, remote_graph=None, type="local", label_uri="rdfs:label"):
+ """Create a new ontology
+
+ Returns
+ -------
+ list of dict
+ Prefixes information
+ """
+ database = Database(self.app, self.session)
+ if not endpoint:
+ endpoint = self.settings.get('triplestore', 'endpoint')
+
+ query = '''
+ INSERT INTO ontologies VALUES(
+ NULL,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?
+ )
+ '''
+
+ database.execute_sql_query(query, (name, uri, short_name, type, dataset_id, graph, label_uri, endpoint, remote_graph))
+
+ query = '''
+ UPDATE datasets SET
+ ontology=1
+ WHERE id=?
+ '''
+
+ database.execute_sql_query(query, (dataset_id,))
+
+ def remove_ontologies(self, ontology_ids):
+ """Remove ontologies
+
+ Returns
+ -------
+ None
+ """
+ # Make sure we only remove the 'ontology' tag to datasets without any ontologies
+ ontologies = self.list_full_ontologies()
+ datasets = defaultdict(set)
+ datasets_to_modify = set()
+ ontos_to_delete = [ontology['id'] for ontology in ontology_ids]
+
+ for onto in ontologies:
+ datasets[onto['dataset_id']].add(onto['id'])
+
+ for key, values in datasets.items():
+ if values.issubset(ontos_to_delete):
+ datasets_to_modify.add(key)
+
+ database = Database(self.app, self.session)
+
+ query = '''
+ DELETE FROM ontologies
+ WHERE id = ?
+ '''
+
+ dataset_query = '''
+ UPDATE datasets SET
+ ontology=0
+ WHERE id=?
+ '''
+
+ for ontology in ontology_ids:
+ database.execute_sql_query(query, (ontology['id'],))
+ if ontology['dataset_id'] in datasets_to_modify:
+ database.execute_sql_query(dataset_query, (ontology['dataset_id'],))
+
+ def test_ols_ontology(self, shortname):
+ base_url = "https://www.ebi.ac.uk/ols/api/ontologies/" + quote_plus(shortname.lower())
+
+ r = requests.get(base_url)
+ return r.status_code == 200
+
+ def autocomplete(self, ontology_uri, ontology_type, query_term, onto_short_name, onto_graph, onto_endpoint, custom_label, remote_graph):
+ """Search in ontology
+
+ Returns
+ -------
+ list of dict
+ Results
+ """
+ # Circular import
+ from askomics.libaskomics.SparqlQuery import SparqlQuery
+ max_results = self.settings.getint("askomics", "autocomplete_max_results", fallback=10)
+
+ if ontology_type == "local":
+ query = SparqlQuery(self.app, self.session, get_graphs=False)
+ # TODO: Actually store the graph in the ontology to quicken search
+ query.set_graphs([onto_graph])
+ query.set_endpoints(set([self.settings.get('triplestore', 'endpoint'), onto_endpoint]))
+ if remote_graph:
+ query.set_remote_graph({onto_endpoint: [remote_graph]})
+
+ return query.autocomplete_local_ontology(ontology_uri, query_term, max_results, custom_label)
+ elif ontology_type == "ols":
+ base_url = "https://www.ebi.ac.uk/ols/api/select"
+ arguments = {
+ "q": query_term,
+ "ontology": quote_plus(onto_short_name.lower()),
+ "rows": max_results,
+ "type": "class",
+ "fieldList": "label"
+ }
+
+ data = []
+
+ try:
+ r = requests.get(base_url, params=arguments, timeout=10)
+
+ if not r.status_code == 200:
+ return data
+
+ res = r.json()
+ if res['response']['docs']:
+ data = [term['label'] for term in res['response']['docs']]
+
+ except requests.exceptions.Timeout:
+ pass
+
+ return data
diff --git a/askomics/libaskomics/PrefixManager.py b/askomics/libaskomics/PrefixManager.py
index 3dc71dfc..493fab53 100644
--- a/askomics/libaskomics/PrefixManager.py
+++ b/askomics/libaskomics/PrefixManager.py
@@ -1,3 +1,4 @@
+from askomics.libaskomics.Database import Database
from askomics.libaskomics.Params import Params
import rdflib
@@ -40,7 +41,9 @@ def __init__(self, app, session):
'rdf:': str(rdflib.RDF),
'rdfs:': str(rdflib.RDFS),
'owl:': str(rdflib.OWL),
- 'xsd:': str(rdflib.XSD)
+ 'xsd:': str(rdflib.XSD),
+ 'skos:': str(rdflib.SKOS),
+ 'dcat:': str(rdflib.DCAT)
}
def get_prefix(self):
@@ -79,4 +82,81 @@ def get_namespace(self, prefix):
try:
return prefix_cc[prefix]
except Exception:
+ prefixes = self.get_custom_prefixes(prefix)
+ if prefixes:
+ return prefixes[0]["namespace"]
return ""
+
+ def get_custom_prefixes(self, prefix=None):
+ """Get custom (admin-defined) prefixes
+
+ Returns
+ -------
+ list of dict
+ Prefixes information
+ """
+ database = Database(self.app, self.session)
+
+ query_args = ()
+ subquery = ""
+
+ if prefix:
+ query_args = (prefix, )
+ subquery = "WHERE prefix = ?"
+
+ query = '''
+ SELECT id, prefix, namespace
+ FROM prefixes
+ {}
+ '''.format(subquery)
+
+ rows = database.execute_sql_query(query, query_args)
+
+ prefixes = []
+ for row in rows:
+ prefix = {
+ 'id': row[0],
+ 'prefix': row[1],
+ 'namespace': row[2],
+ }
+ prefixes.append(prefix)
+
+ return prefixes
+
+ def add_custom_prefix(self, prefix, namespace):
+ """Create a new custom (admin-defined) prefixes
+
+ Returns
+ -------
+ list of dict
+ Prefixes information
+ """
+ database = Database(self.app, self.session)
+
+ query = '''
+ INSERT INTO prefixes VALUES(
+ NULL,
+ ?,
+ ?
+ )
+ '''
+
+ database.execute_sql_query(query, (prefix, namespace,))
+
+ def remove_custom_prefixes(self, prefixes_id):
+ """Create a new custom (admin-defined) prefixes
+
+ Returns
+ -------
+ list of dict
+ Prefixes information
+ """
+ database = Database(self.app, self.session)
+
+ query = '''
+ DELETE FROM prefixes
+ WHERE id = ?
+ '''
+
+ for prefix_id in prefixes_id:
+ database.execute_sql_query(query, (prefix_id,))
diff --git a/askomics/libaskomics/RdfFile.py b/askomics/libaskomics/RdfFile.py
index 0954d91f..008bbd3b 100644
--- a/askomics/libaskomics/RdfFile.py
+++ b/askomics/libaskomics/RdfFile.py
@@ -14,7 +14,7 @@ class RdfFile(File):
Public or private dataset
"""
- def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None):
+ def __init__(self, app, session, file_info, host_url=None, external_endpoint=None, custom_uri=None, external_graph=None):
"""init
Parameters
@@ -28,7 +28,7 @@ def __init__(self, app, session, file_info, host_url=None, external_endpoint=Non
host_url : None, optional
AskOmics url
"""
- File.__init__(self, app, session, file_info, host_url, external_endpoint=external_endpoint, custom_uri=custom_uri)
+ File.__init__(self, app, session, file_info, host_url, external_endpoint=external_endpoint, custom_uri=custom_uri, external_graph=external_graph)
self.type_dict = {
"rdf/ttl": "turtle",
@@ -40,7 +40,7 @@ def set_preview(self):
"""Summary"""
pass
- def get_location(self):
+ def get_location_and_remote_graph(self):
"""Get location of data if specified
Returns
@@ -50,10 +50,19 @@ def get_location(self):
"""
graph = RdfGraph(self.app, self.session)
graph.parse(self.path, format=self.type_dict[self.type])
- triple = (None, self.prov.atLocation, None)
- for s, p, o in graph.graph.triples(triple):
- return str(o)
- return None
+ triple_loc = (None, self.prov.atLocation, None)
+ triple_graph = (None, self.dcat.Dataset, None)
+ loc = None
+ remote_graph = None
+ for s, p, o in graph.graph.triples(triple_loc):
+ loc = str(o)
+ break
+
+ for s, p, o in graph.graph.triples(triple_graph):
+ remote_graph = str(o)
+ break
+
+ return loc, remote_graph
def get_preview(self):
"""Get a preview of the frist 100 lines of a ttl file
@@ -69,6 +78,12 @@ def get_preview(self):
for x in range(1, 100):
head += ttl_file.readline()
+ location = None
+ try:
+ location, remote_graph = self.get_location_and_remote_graph()
+ except Exception as e:
+ self.error_message = str(e)
+
return {
'type': self.type,
'id': self.id,
@@ -77,13 +92,15 @@ def get_preview(self):
'error_message': self.error_message,
'data': {
'preview': head,
- 'location': self.get_location()
+ 'location': location,
+ 'remote_graph': remote_graph
}
}
def delete_metadata_location(self):
"""Delete metadata from data"""
self.graph_chunk.remove((None, self.prov.atLocation, None))
+ self.graph_chunk.remove((None, self.dcat.Dataset, None))
def integrate(self, public=False):
"""Integrate the file into the triplestore
diff --git a/askomics/libaskomics/RdfGraph.py b/askomics/libaskomics/RdfGraph.py
index 0bd40e0b..0024a3ed 100644
--- a/askomics/libaskomics/RdfGraph.py
+++ b/askomics/libaskomics/RdfGraph.py
@@ -40,6 +40,7 @@ def __init__(self, app, session):
self.graph.bind('faldo', "http://biohackathon.org/resource/faldo/")
self.graph.bind('dc', 'http://purl.org/dc/elements/1.1/')
self.graph.bind('prov', 'http://www.w3.org/ns/prov#')
+ self.graph.bind('dcat', 'http://www.w3.org/ns/dcat#')
self.ntriple = 0
self.percent = None
diff --git a/askomics/libaskomics/Result.py b/askomics/libaskomics/Result.py
index 07e74ca5..d48f7725 100644
--- a/askomics/libaskomics/Result.py
+++ b/askomics/libaskomics/Result.py
@@ -389,6 +389,33 @@ def save_in_db(self):
return self.id
+ def populate_db(self, graphs, endpoints):
+ """Update status of results in db
+
+ Parameters
+ ----------
+ query : bool, optional
+ True if error during integration
+ error_message : bool, optional
+ Error string if error is True
+ """
+
+ database = Database(self.app, self.session)
+
+ query = '''
+ UPDATE results SET
+ graphs_and_endpoints=?
+ WHERE user_id=? AND id=?
+ '''
+
+ variables = [
+ json.dumps({"graphs": graphs, "endpoints": endpoints}),
+ self.session["user"]["id"],
+ self.id
+ ]
+
+ database.execute_sql_query(query, tuple(variables))
+
def update_public_status(self, public):
"""Change public status
@@ -471,7 +498,7 @@ def update_db_status(self, status, size=None, update_celery=False, update_date=F
def rollback(self):
"""Delete file"""
- self.delete_file_from_filesystem(self)
+ self.delete_file_from_filesystem()
def delete_result(self):
"""Remove results from db and filesystem"""
diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py
index ae098b87..57d2c29a 100644
--- a/askomics/libaskomics/SparqlQuery.py
+++ b/askomics/libaskomics/SparqlQuery.py
@@ -6,6 +6,8 @@
from askomics.libaskomics.SparqlQueryLauncher import SparqlQueryLauncher
from askomics.libaskomics.Utils import Utils
+from collections import defaultdict
+
class SparqlQuery(Params):
"""Format a sparql query
@@ -18,7 +20,7 @@ class SparqlQuery(Params):
all public graph
"""
- def __init__(self, app, session, json_query=None):
+ def __init__(self, app, session, json_query=None, get_graphs=False):
"""init
Parameters
@@ -35,6 +37,7 @@ def __init__(self, app, session, json_query=None):
self.graphs = []
self.endpoints = []
+ self.remote_graphs = defaultdict(list)
self.selects = []
self.federated = False
@@ -44,8 +47,9 @@ def __init__(self, app, session, json_query=None):
self.local_endpoint_f = self.settings.get('federation', 'local_endpoint')
except Exception:
pass
-
- self.set_graphs_and_endpoints()
+ # No need to call this twice if we need it later (sparql queries)
+ if get_graphs:
+ self.set_graphs_and_endpoints()
def set_graphs(self, graphs):
"""Set graphs
@@ -67,6 +71,16 @@ def set_endpoints(self, endpoints):
"""
self.endpoints = endpoints
+ def set_remote_graph(self, remote_graphs):
+ """Set endpoints
+
+ Parameters
+ ----------
+ endpoints : list
+ Endpoints
+ """
+ self.remote_graphs = remote_graphs
+
def is_federated(self):
"""Return True if there is more than 1 endpoint
@@ -275,7 +289,7 @@ def get_default_query_with_prefix(self):
self.get_default_query()
)
- def format_query(self, query, limit=30, replace_froms=True, federated=False):
+ def format_query(self, query, limit=30, replace_froms=True, federated=False, ignore_single_tenant=True):
"""Format the Sparql query
- remove all FROM
@@ -295,11 +309,13 @@ def format_query(self, query, limit=30, replace_froms=True, federated=False):
formatted sparql query
"""
froms = ''
- if replace_froms:
- froms = self.get_froms()
if federated:
- federated_line = "{}\n{}".format(self.get_federated_line(), self.get_federated_froms())
+ federated_line = "" if self.settings.getboolean("askomics", "single_tenant", fallback=False) else "{}\n{}".format(self.get_federated_line(), self.get_federated_froms())
+ federated_graphs_string = self.get_federated_remote_from_graphs()
+ else:
+ if replace_froms and (not self.settings.getboolean("askomics", "single_tenant", fallback=False)):
+ froms = self.get_froms()
query_lines = query.split('\n')
@@ -310,6 +326,7 @@ def format_query(self, query, limit=30, replace_froms=True, federated=False):
if not line.upper().lstrip().startswith('FROM') and not line.upper().lstrip().startswith('LIMIT') and not line.upper().lstrip().startswith('@FEDERATE'):
if line.upper().lstrip().startswith('SELECT') and federated:
new_query += "\n{}\n".format(federated_line)
+ new_query += "\n{}\n".format(federated_graphs_string)
new_query += '\n{}'.format(line)
# Add new FROM
if line.upper().lstrip().startswith('SELECT'):
@@ -374,6 +391,22 @@ def get_federated_froms_from_graphs(self, graphs):
from_string = "@from <{}>".format(self.local_endpoint_f)
for graph in graphs:
from_string += " <{}>".format(graph)
+ return from_string
+
+ def get_federated_remote_from_graphs(self):
+ """Get @from string fir the federated query engine
+
+ Returns
+ -------
+ string
+ The from string
+ """
+ from_string = ""
+
+ for endpoint in self.endpoints:
+ remote_graphs = self.remote_graphs.get(endpoint, [])
+ if len(remote_graphs) == 1:
+ from_string += "\n@graph <{}> <{}>".format(endpoint, remote_graphs[0])
return from_string
@@ -391,7 +424,7 @@ def get_endpoints_string(self):
return endpoints_string
- def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None):
+ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None, ontologies={}):
"""Get all public and private graphs containing the given entities
Parameters
@@ -399,26 +432,33 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None):
entities : list, optional
list of entity uri
"""
- substrlst = []
filter_entity_string = ''
if entities:
- for entity in entities:
- substrlst.append("?entity_uri = <{}>".format(entity))
- filter_entity_string = 'FILTER (' + ' || '.join(substrlst) + ')'
+ substr = ",".join(["<{}>".format(entity) for entity in entities])
+ filter_entity_string = 'FILTER (?entity_uri IN( ' + substr + ' ))'
filter_public_string = 'FILTER (?public = )'
if 'user' in self.session:
filter_public_string = 'FILTER (?public = || ?creator = <{}>)'.format(self.session["user"]["username"])
query = '''
- SELECT DISTINCT ?graph ?endpoint
+ SELECT DISTINCT ?graph ?endpoint ?entity_uri ?remote_graph
WHERE {{
+ ?graph_abstraction askomics:public ?public .
+ ?graph_abstraction dc:creator ?creator .
?graph askomics:public ?public .
?graph dc:creator ?creator .
+ GRAPH ?graph_abstraction {{
+ ?graph_abstraction prov:atLocation ?endpoint .
+ OPTIONAL {{?graph_abstraction dcat:Dataset ?remote_graph .}}
+ ?entity_uri a ?askomics_type .
+ }}
GRAPH ?graph {{
- ?graph prov:atLocation ?endpoint .
- ?entity_uri a askomics:entity .
+ {{ [] a ?entity_uri . }}
+ UNION
+ {{ ?entity_uri a ?askomics_type . }}
}}
+ VALUES ?askomics_type {{askomics:entity askomics:ontology}}
{}
{}
}}
@@ -428,11 +468,17 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None):
header, results = query_launcher.process_query(self.prefix_query(query))
self.graphs = []
self.endpoints = []
+ self.remote_graphs = defaultdict(list)
for res in results:
if not graphs or res["graph"] in graphs:
- self.graphs.append(res["graph"])
+ # Override with onto graph if matching uri
+ if ontologies.get(res['entity_uri']):
+ graph = ontologies[res['entity_uri']]['graph']
+ else:
+ graph = res["graph"]
+ self.graphs.append(graph)
- # If local triplestore url is not accessible by federetad query engine
+ # If local triplestore url is not accessible by federated query engine
if res["endpoint"] == self.settings.get('triplestore', 'endpoint') and self.local_endpoint_f is not None:
endpoint = self.local_endpoint_f
else:
@@ -440,10 +486,143 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None):
if not endpoints or endpoint in endpoints:
self.endpoints.append(endpoint)
+ if res.get("remote_graph"):
+ self.remote_graphs[endpoint].append(res.get("remote_graph"))
self.endpoints = Utils.unique(self.endpoints)
self.federated = len(self.endpoints) > 1
+ def get_uri_parameters(self, uri, endpoints):
+ """Get parameters for a specific data URI
+
+ Parameters
+ ----------
+ uri : string
+ URI to search
+
+ Returns
+ -------
+ dict
+ The corresponding parameters
+ """
+ raw_query = '''
+ SELECT DISTINCT ?predicate ?object ?faldo_value ?faldo_relation
+ WHERE {{
+ ?URI ?predicate ?object .
+ ?URI a ?entitytype .
+
+ FILTER(! STRSTARTS(STR(?predicate), STR(askomics:)))
+ OPTIONAL {{
+
+ ?faldo_uri rdfs:domain ?entitytype .
+ ?faldo_uri rdfs:label ?attribute_label .
+
+ OPTIONAL {{
+ ?object faldo:begin/faldo:position ?faldo_value .
+ ?faldo_uri rdf:type askomics:faldoStart
+ }}
+
+ OPTIONAL {{
+ ?object faldo:end/faldo:position ?faldo_value .
+ ?faldo_uri rdf:type askomics:faldoEnd
+ }}
+
+ OPTIONAL {{
+ ?object faldo:begin/faldo:reference/rdfs:label ?faldo_value .
+ ?faldo_uri rdf:type askomics:faldoReference
+ }}
+
+ OPTIONAL {{
+ ?object faldo:begin/rdf:type ?Gene1_strandCategory .
+ ?Gene1_strand_faldoStrand a ?Gene1_strandCategory .
+ ?Gene1_strand_faldoStrand rdfs:label ?faldo_value .
+ ?faldo_uri rdf:type askomics:faldoStrand .
+ }}
+
+ OPTIONAL {{
+ ?faldo_uri askomics:uri ?node_uri
+ }}
+
+ VALUES ?predicate {{faldo:location}}
+ }}
+ VALUES ?URI {{{}}}
+ BIND(IF(isBlank(?faldo_uri), ?node_uri ,?faldo_uri) as ?faldo_relation)
+ }}
+ '''.format(uri)
+
+ federated = self.is_federated()
+ replace_froms = self.replace_froms()
+
+ raw_query = self.prefix_query(raw_query)
+
+ sparql = self.format_query(raw_query, replace_froms=replace_froms, federated=federated)
+
+ query_launcher = SparqlQueryLauncher(self.app, self.session, get_result_query=True, federated=federated, endpoints=endpoints)
+ _, data = query_launcher.process_query(sparql)
+
+ formated_data = []
+ for row in data:
+
+ predicate = row['predicate']
+ object = row['object']
+
+ if row.get('faldo_relation'):
+ predicate = row.get("faldo_relation")
+
+ if row.get('faldo_value'):
+ object = row.get('faldo_value')
+
+ formated_data.append({
+ 'predicate': predicate,
+ 'object': object,
+ })
+
+ return formated_data
+
+ def autocomplete_local_ontology(self, uri, query, max_terms, label):
+ """Get results for a specific query
+
+ Parameters
+ ----------
+ uri : string
+ ontology uri
+ query : string
+ query to search
+
+ Returns
+ -------
+ dict
+ The corresponding parameters
+ """
+
+ subquery = ""
+
+ if query:
+ subquery = 'FILTER(contains(lcase(?label), "{}"))'.format(query.lower())
+ raw_query = '''
+ SELECT DISTINCT ?label
+ WHERE {{
+ ?uri rdf:type owl:Class .
+ ?uri {} ?label .
+ {}
+ }}
+ '''.format(label, subquery)
+
+ raw_query = self.prefix_query(raw_query)
+
+ is_federated = self.is_federated()
+
+ sparql = self.format_query(raw_query, limit=max_terms, replace_froms=True, federated=is_federated)
+
+ query_launcher = SparqlQueryLauncher(self.app, self.session, get_result_query=True, federated=is_federated)
+ _, data = query_launcher.process_query(sparql)
+
+ formated_data = []
+ for row in data:
+ formated_data.append(row['label'])
+
+ return formated_data
+
def format_sparql_variable(self, name):
"""Format a name into a sparql variable by remove spacial char and add a ?
@@ -873,6 +1052,8 @@ def build_query_from_json(self, preview=False, for_editor=False):
for_editor : bool, optional
Remove FROMS and @federate
"""
+ # Circular import
+ from askomics.libaskomics.OntologyManager import OntologyManager
entities = []
attributes = {}
linked_attributes = []
@@ -890,14 +1071,19 @@ def build_query_from_json(self, preview=False, for_editor=False):
var_to_replace = []
+ ontologies = {}
+ om = OntologyManager(self.app, self.session)
+
# Browse attributes to get entities
for attr in self.json["attr"]:
entities = entities + attr["entityUris"]
+ if attr["type"] == "uri" and attr.get("ontology", False) is True and not attr["entityUris"][0] in ontologies:
+ ontologies[attr["entityUris"][0]] = om.get_ontology(uri=attr["entityUris"][0])
entities = list(set(entities)) # uniq list
# Set graphs in function of entities needed
- self.set_graphs_and_endpoints(entities=entities)
+ self.set_graphs_and_endpoints(entities=entities, ontologies=ontologies)
# self.log.debug(self.json)
@@ -1009,7 +1195,20 @@ def build_query_from_json(self, preview=False, for_editor=False):
# Classic relation
else:
- relation = "<{}>".format(link["uri"])
+ # Manage ontology stuff
+ inverse = ""
+ recurrence = ""
+ relation = link["uri"]
+
+ if relation.startswith("^"):
+ inverse = "^"
+ relation = relation.lstrip("^")
+
+ if relation.endswith("*"):
+ recurrence = "*"
+ relation = relation.rstrip("*")
+
+ relation = inverse + "<{}>".format(relation) + recurrence
triple = {
"subject": source,
"predicate": relation,
@@ -1031,7 +1230,6 @@ def build_query_from_json(self, preview=False, for_editor=False):
# Browse attributes
for attribute in self.json["attr"]:
-
# Get blockid and sblockid of the attribute node
block_id, sblock_id, pblock_ids = self.get_block_ids(attribute["nodeId"])
@@ -1040,13 +1238,20 @@ def build_query_from_json(self, preview=False, for_editor=False):
subject = self.format_sparql_variable("{}{}_uri".format(attribute["entityLabel"], attribute["nodeId"]))
predicate = attribute["uri"]
obj = "<{}>".format(attribute["entityUris"][0])
- if not self.is_bnode(attribute["entityUris"][0], self.json["nodes"]):
+ if not (self.is_bnode(attribute["entityUris"][0], self.json["nodes"]) or attribute.get("ontology", False) is True):
self.store_triple({
"subject": subject,
"predicate": predicate,
"object": obj,
"optional": False
}, block_id, sblock_id, pblock_ids)
+ if attribute.get("ontology", False) is True:
+ self.store_triple({
+ "subject": subject,
+ "predicate": predicate,
+ "object": "owl:Class",
+ "optional": False
+ }, block_id, sblock_id, pblock_ids)
if attribute["visible"]:
self.selects.append(subject)
@@ -1114,6 +1319,8 @@ def build_query_from_json(self, preview=False, for_editor=False):
subject = self.format_sparql_variable("{}{}_uri".format(attribute["entityLabel"], attribute["nodeId"]))
if attribute["uri"] == "rdfs:label":
predicate = attribute["uri"]
+ if ontologies.get(attribute["entityUris"][0]):
+ predicate = ontologies[attribute["entityUris"][0]]["label_uri"]
else:
predicate = "<{}>".format(attribute["uri"])
@@ -1300,9 +1507,10 @@ def build_query_from_json(self, preview=False, for_editor=False):
))
var_to_replace.append((category_value_uri, var_2))
- from_string = self.get_froms_from_graphs(self.graphs)
+ from_string = "" if self.settings.getboolean("askomics", "single_tenant", fallback=False) else self.get_froms_from_graphs(self.graphs)
federated_from_string = self.get_federated_froms_from_graphs(self.graphs)
endpoints_string = self.get_endpoints_string()
+ federated_graphs_string = self.get_federated_remote_from_graphs()
# Linked attributes: replace SPARQL variable target by source
self.replace_variables_in_blocks(var_to_replace)
@@ -1332,6 +1540,7 @@ def build_query_from_json(self, preview=False, for_editor=False):
query = """
{endpoints}
{federated}
+{remote_graphs}
SELECT DISTINCT {selects}
WHERE {{
@@ -1343,6 +1552,7 @@ def build_query_from_json(self, preview=False, for_editor=False):
""".format(
endpoints=endpoints_string,
federated=federated_from_string,
+ remote_graphs=federated_graphs_string,
selects=' '.join(self.selects),
triples='\n '.join([self.triple_dict_to_string(triple_dict) for triple_dict in self.triples]),
blocks='\n '.join([self.triple_block_to_string(triple_block) for triple_block in self.triples_blocks]),
diff --git a/askomics/libaskomics/SparqlQueryLauncher.py b/askomics/libaskomics/SparqlQueryLauncher.py
index aae8cc51..64fe2802 100644
--- a/askomics/libaskomics/SparqlQueryLauncher.py
+++ b/askomics/libaskomics/SparqlQueryLauncher.py
@@ -44,6 +44,7 @@ def __init__(self, app, session, get_result_query=False, federated=False, endpoi
except Exception:
pass
+ local = False
# Use the federated query engine
if federated:
self.federated = True
@@ -65,16 +66,20 @@ def __init__(self, app, session, get_result_query=False, federated=False, endpoi
self.triplestore = self.settings.get('triplestore', 'triplestore')
self.url_endpoint = self.settings.get('triplestore', 'endpoint')
self.url_updatepoint = self.settings.get('triplestore', 'updatepoint')
+ local = True
+
+ self.endpoint = SPARQLWrapper(self.url_endpoint, self.url_updatepoint)
+
+ if local:
try:
self.endpoint.setCredentials(
self.settings.get('triplestore', 'username'),
self.settings.get('triplestore', 'password')
)
+ self.endpoint.setHTTPAuth(self.settings.get('triplestore', 'http_auth', fallback="basic"))
except Exception:
pass
- self.endpoint = SPARQLWrapper(self.url_endpoint, self.url_updatepoint)
-
def load_data(self, file_name, graph, host_url):
"""Load data in function of the triplestore
@@ -220,7 +225,7 @@ def insert_data(self, ttl, graph, metadata=False):
TYPE
query result
"""
- triples = self.get_triples_from_graph(ttl) if metadata else ttl.serialize(format='nt').decode("utf-8")
+ triples = self.get_triples_from_graph(ttl) if metadata else ttl.serialize(format='nt')
query = '''
INSERT {{
@@ -305,8 +310,8 @@ def execute_query(self, query, disable_log=False, isql_api=False):
if self.endpoint.isSparqlUpdateRequest():
self.endpoint.setMethod('POST')
# Virtuoso hack
- if self.triplestore == 'virtuoso':
- self.endpoint.queryType = "SELECT"
+ # if self.triplestore == 'virtuoso':
+ # self.endpoint.queryType = "SELECT"
results = self.endpoint.query()
# Select
diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py
index 50557c6c..ced8a54e 100644
--- a/askomics/libaskomics/TriplestoreExplorer.py
+++ b/askomics/libaskomics/TriplestoreExplorer.py
@@ -208,12 +208,14 @@ def get_abstraction(self):
"""
insert, abstraction = self.get_cached_asbtraction()
+ single_tenant = self.settings.getboolean("askomics", "single_tenant", fallback=False)
+
# No abstraction entry in database, create it
if not abstraction:
abstraction = {
- "entities": self.get_abstraction_entities(),
- "attributes": self.get_abstraction_attributes(),
- "relations": self.get_abstraction_relations()
+ "entities": self.get_abstraction_entities(single_tenant),
+ "attributes": self.get_abstraction_attributes(single_tenant),
+ "relations": self.get_abstraction_relations(single_tenant)
}
# Cache abstraction in DB, only for logged users
@@ -305,7 +307,7 @@ def uncache_abstraction(self, public=True, force=False):
database.execute_sql_query(query, sql_var)
- def get_abstraction_entities(self):
+ def get_abstraction_entities(self, single_tenant=False):
"""Get abstraction entities
Returns
@@ -328,7 +330,7 @@ def get_abstraction_entities(self):
GRAPH ?graph {{
?graph prov:atLocation ?endpoint .
?entity_uri a ?entity_type .
- VALUES ?entity_type {{ askomics:entity askomics:bnode }} .
+ VALUES ?entity_type {{ askomics:entity askomics:bnode askomics:ontology}} .
# Faldo
OPTIONAL {{
?entity_uri a ?entity_faldo .
@@ -362,6 +364,7 @@ def get_abstraction_entities(self):
"label": label,
"instancesHaveLabels": True if "have_no_label" not in result else False if result["have_no_label"] == "1" else True,
"faldo": True if "entity_faldo" in result else False,
+ "ontology": True if result["entity_type"] == "{}ontology".format(self.settings.get("triplestore", "namespace_internal")) else False,
"endpoints": [result["endpoint"]],
"graphs": [result["graph"]],
}
@@ -378,7 +381,7 @@ def get_abstraction_entities(self):
return entities
- def get_abstraction_attributes(self):
+ def get_abstraction_attributes(self, single_tenant=False):
"""Get user abstraction attributes from the triplestore
Returns
@@ -407,13 +410,16 @@ def get_abstraction_attributes(self):
?graph askomics:public ?public .
?graph dc:creator ?creator .
GRAPH ?graph {{
- ?attribute_uri a ?attribute_type .
+ ?node a ?attribute_type .
VALUES ?attribute_type {{ owl:DatatypeProperty askomics:AskomicsCategory }}
- ?attribute_uri rdfs:label ?attribute_label .
- ?attribute_uri rdfs:range ?attribute_range .
+ ?node rdfs:label ?attribute_label .
+ ?node rdfs:range ?attribute_range .
+ # Retrocompatibility
+ OPTIONAL {{?node askomics:uri ?new_attribute_uri}}
+ BIND( IF(isBlank(?node),?new_attribute_uri, ?node) as ?attribute_uri )
# Faldo
OPTIONAL {{
- ?attribute_uri a ?attribute_faldo .
+ ?node a ?attribute_faldo .
VALUES ?attribute_faldo {{ askomics:faldoStart askomics:faldoEnd askomics:faldoStrand askomics:faldoReference }}
}}
# Categories (DK)
@@ -424,10 +430,10 @@ def get_abstraction_attributes(self):
}}
# Attribute of entity (or motherclass of entity)
{{
- ?attribute_uri rdfs:domain ?mother .
+ ?node rdfs:domain ?mother .
?entity_uri rdfs:subClassOf ?mother .
}} UNION {{
- ?attribute_uri rdfs:domain ?entity_uri .
+ ?node rdfs:domain ?entity_uri .
}}
FILTER (
?public = {}
@@ -441,13 +447,13 @@ def get_abstraction_attributes(self):
attributes = []
for result in data:
- # Attributes
- if "attribute_uri" in result and "attribute_label" in result and result["attribute_type"] != "{}AskomicsCategory".format(self.settings.get("triplestore", "namespace_internal")) and result["attribute_range"] in litterals:
- attr_tpl = (result["attribute_uri"], result["entity_uri"])
+ attribute_uri = result.get("attribute_uri")
+ if attribute_uri and "attribute_label" in result and result["attribute_type"] != "{}AskomicsCategory".format(self.settings.get("triplestore", "namespace_internal")) and result["attribute_range"] in litterals:
+ attr_tpl = (attribute_uri, result["entity_uri"])
if attr_tpl not in attributes_list:
attributes_list.append(attr_tpl)
attribute = {
- "uri": result["attribute_uri"],
+ "uri": attribute_uri,
"label": result["attribute_label"],
"graphs": [result["graph"], ],
"entityUri": result["entity_uri"],
@@ -465,12 +471,12 @@ def get_abstraction_attributes(self):
index_attribute = attributes_list.index(attr_tpl)
# Categories
- if "attribute_uri" in result and result["attribute_type"] == "{}AskomicsCategory".format(self.settings.get("triplestore", "namespace_internal")) and "category_value_uri" in result:
- attr_tpl = (result["attribute_uri"], result["entity_uri"])
+ if attribute_uri and result["attribute_type"] == "{}AskomicsCategory".format(self.settings.get("triplestore", "namespace_internal")) and "category_value_uri" in result:
+ attr_tpl = (attribute_uri, result["entity_uri"])
if attr_tpl not in attributes_list:
attributes_list.append(attr_tpl)
attribute = {
- "uri": result["attribute_uri"],
+ "uri": attribute_uri,
"label": result["attribute_label"],
"graphs": [result["graph"], ],
"entityUri": result["entity_uri"],
@@ -497,7 +503,7 @@ def get_abstraction_attributes(self):
return attributes
- def get_abstraction_relations(self):
+ def get_abstraction_relations(self, single_tenant=False):
"""Get user abstraction relations from the triplestore
Returns
@@ -513,24 +519,27 @@ def get_abstraction_relations(self):
query_builder = SparqlQuery(self.app, self.session)
query = '''
- SELECT DISTINCT ?graph ?entity_uri ?entity_faldo ?entity_label ?node_type ?attribute_uri ?attribute_faldo ?attribute_label ?attribute_range ?property_uri ?property_faldo ?property_label ?range_uri ?category_value_uri ?category_value_label
+ SELECT DISTINCT ?graph ?entity_uri ?entity_faldo ?entity_label ?attribute_uri ?attribute_faldo ?attribute_label ?attribute_range ?property_uri ?property_faldo ?property_label ?range_uri ?category_value_uri ?category_value_label
WHERE {{
# Graphs
?graph askomics:public ?public .
?graph dc:creator ?creator .
GRAPH ?graph {{
# Property (relations and categories)
- ?property_uri a owl:ObjectProperty .
- ?property_uri a askomics:AskomicsRelation .
- ?property_uri rdfs:label ?property_label .
- ?property_uri rdfs:range ?range_uri .
+ ?node a owl:ObjectProperty .
+ ?node a askomics:AskomicsRelation .
+ ?node rdfs:label ?property_label .
+ ?node rdfs:range ?range_uri .
+ # Retrocompatibility
+ OPTIONAL {{?node askomics:uri ?new_property_uri}}
+ BIND( IF(isBlank(?node), ?new_property_uri, ?node) as ?property_uri)
}}
# Relation of entity (or motherclass of entity)
{{
- ?property_uri rdfs:domain ?mother .
+ ?node rdfs:domain ?mother .
?entity_uri rdfs:subClassOf ?mother .
}} UNION {{
- ?property_uri rdfs:domain ?entity_uri .
+ ?node rdfs:domain ?entity_uri .
}}
FILTER (
?public = {}
@@ -542,15 +551,15 @@ def get_abstraction_relations(self):
relations_list = []
relations = []
-
for result in data:
# Relation
if "property_uri" in result:
- rel_tpl = (result["property_uri"], result["entity_uri"], result["range_uri"])
+ property_uri = result.get("property_uri")
+ rel_tpl = (property_uri, result["entity_uri"], result["range_uri"])
if rel_tpl not in relations_list:
relations_list.append(rel_tpl)
relation = {
- "uri": result["property_uri"],
+ "uri": property_uri,
"label": result["property_label"],
"graphs": [result["graph"], ],
"source": result["entity_uri"],
diff --git a/askomics/react/src/components/autocomplete.jsx b/askomics/react/src/components/autocomplete.jsx
new file mode 100644
index 00000000..452e9575
--- /dev/null
+++ b/askomics/react/src/components/autocomplete.jsx
@@ -0,0 +1,147 @@
+import React, { Component} from 'react'
+import axios from 'axios'
+import PropTypes from 'prop-types'
+import { Input } from 'reactstrap'
+import Autosuggest from 'react-autosuggest';
+
+
+export default class Autocomplete extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ ontologyShort: this.getAutoComplete(),
+ maxResults: this.props.config.autocompleteMaxResults,
+ options: []
+ }
+
+ this.handleFilterValue = this.props.handleFilterValue.bind(this)
+ this.autocompleteOntology = this.autocompleteOntology.bind(this)
+ this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
+ this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
+ this.cancelRequest
+ this.handleOntoValue = this.handleOntoValue.bind(this)
+ this.WAIT_INTERVAL = 500
+ this.timerID
+ }
+
+ getAutoComplete () {
+ let selectedOnto = this.props.config.ontologies.find(onto => onto.uri == this.props.entityUri)
+ if (selectedOnto){
+ return selectedOnto.short_name
+ }
+ return ""
+ }
+
+ autocompleteOntology (value) {
+ if (this.state.ontologyShort.length == 0){ return }
+
+ let userInput = value
+ let requestUrl = '/api/ontology/' + this.state.ontologyShort + "/autocomplete"
+
+ if (value.length < 3) { return }
+
+ axios.get(requestUrl, {baseURL: this.props.config.proxyPath, params:{q: userInput}, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
+ .then(response => {
+ this.setState({
+ options: response.data.results
+ })
+ })
+ .catch(error => {
+ console.log(error, error.response.data.errorMessage)
+ this.setState({
+ error: true,
+ errorMessage: error.response.data.errorMessage,
+ status: error.response.status,
+ waiting: false
+ })
+ })
+ }
+
+
+ handleOntoValue (event, value) {
+ this.handleFilterValue({target:{value: value.newValue, id: this.props.attributeId}})
+ }
+
+
+ renderSuggestion (suggestion, {query, isHighlighted}) {
+ let textArray = suggestion.split(RegExp(query, "gi"));
+ let match = suggestion.match(RegExp(query, "gi"));
+
+ return (
+
+ {textArray.map((item, index) => (
+
+ {item}
+ {index !== textArray.length - 1 && match && (
+ {match[index]}
+ )}
+
+ ))}
+
+ );
+ }
+
+ onSuggestionsClearRequested () {
+ this.setState({
+ options: []
+ })
+ }
+
+ getSuggestionValue (suggestion) {
+ return suggestion
+ };
+
+ onSuggestionsFetchRequested ( value ){
+ clearTimeout(this.timerID)
+ this.timerID = setTimeout(() => {
+ this.autocompleteOntology(value.value)
+ }, this.WAIT_INTERVAL)
+ };
+
+
+ renderInputComponent (inputProps){
+ return(
+
+
+
+ )
+ }
+
+
+ shouldRenderSuggestions(value, reason){
+ return value.trim().length > 2;
+ }
+
+ render () {
+
+ let value = this.props.filterValue
+
+ let inputProps = {
+ placeholder: '',
+ value,
+ onChange: this.handleOntoValue
+ };
+
+ return (
+
+ )
+
+ }
+}
+
+Autocomplete.propTypes = {
+ handleFilterValue: PropTypes.func,
+ entityUri: PropTypes.string,
+ attributeId: PropTypes.number,
+ filterValue: PropTypes.string,
+ config: PropTypes.object,
+}
diff --git a/askomics/react/src/navbar.jsx b/askomics/react/src/navbar.jsx
index ff3062fc..01b5e457 100644
--- a/askomics/react/src/navbar.jsx
+++ b/askomics/react/src/navbar.jsx
@@ -43,6 +43,8 @@ export default class AskoNavbar extends Component {
adminLinks = (
Admin
+ Prefixes
+ Ontologies
)
}
@@ -104,4 +106,4 @@ export default class AskoNavbar extends Component {
AskoNavbar.propTypes = {
waitForStart: PropTypes.bool,
config: PropTypes.object
-}
\ No newline at end of file
+}
diff --git a/askomics/react/src/routes.jsx b/askomics/react/src/routes.jsx
index 6555c070..94e92c7e 100644
--- a/askomics/react/src/routes.jsx
+++ b/askomics/react/src/routes.jsx
@@ -14,6 +14,8 @@ import Logout from './routes/login/logout'
import PasswordReset from './routes/login/passwordreset'
import Account from './routes/account/account'
import Admin from './routes/admin/admin'
+import Prefixes from './routes/admin/prefixes'
+import Ontologies from './routes/admin/ontologies'
import Sparql from './routes/sparql/sparql'
import FormQuery from './routes/form/query'
import FormEditQuery from './routes/form_edit/query'
@@ -44,7 +46,10 @@ export default class Routes extends Component {
gitUrl: null,
disableIntegration: null,
namespaceData: null,
- namespaceInternal: null
+ namespaceInternal: null,
+ ontologies: [],
+ singleTenant: false,
+ autocompleteMaxResults: 10
}
}
this.cancelRequest
@@ -113,6 +118,8 @@ export default class Routes extends Component {
this.setState(p)} {...props}/>}/>
( this.setState(p)} />)} />
( this.setState(p)} />)} />
+ ( this.setState(p)} />)} />
+ ( this.setState(p)} />)} />
diff --git a/askomics/react/src/routes/admin/admin.jsx b/askomics/react/src/routes/admin/admin.jsx
index 17cebd31..9f4bfb63 100644
--- a/askomics/react/src/routes/admin/admin.jsx
+++ b/askomics/react/src/routes/admin/admin.jsx
@@ -43,7 +43,7 @@ export default class Admin extends Component {
instanceUrl: "",
usersSelected: [],
filesSelected: [],
- datasetsSelected: []
+ datasetsSelected: [],
}
this.handleChangeUserInput = this.handleChangeUserInput.bind(this)
this.handleChangeFname = this.handleChangeFname.bind(this)
@@ -404,6 +404,7 @@ export default class Admin extends Component {
this.setState(p)} queriesLoading={this.state.queriesLoading} />
+
)
}
diff --git a/askomics/react/src/routes/admin/datasetstable.jsx b/askomics/react/src/routes/admin/datasetstable.jsx
index e961b6b4..dd199566 100644
--- a/askomics/react/src/routes/admin/datasetstable.jsx
+++ b/askomics/react/src/routes/admin/datasetstable.jsx
@@ -99,7 +99,7 @@ render () {
return (
-
+
)
diff --git a/askomics/react/src/routes/admin/ontologies.jsx b/askomics/react/src/routes/admin/ontologies.jsx
new file mode 100644
index 00000000..d7b97511
--- /dev/null
+++ b/askomics/react/src/routes/admin/ontologies.jsx
@@ -0,0 +1,365 @@
+import React, { Component } from 'react'
+import axios from 'axios'
+import {Button, Form, FormGroup, Label, Input, Alert, Row, Col, CustomInput } from 'reactstrap'
+import BootstrapTable from 'react-bootstrap-table-next'
+import paginationFactory from 'react-bootstrap-table2-paginator'
+import update from 'react-addons-update'
+import PropTypes from 'prop-types'
+import Utils from '../../classes/utils'
+import { Redirect } from 'react-router-dom'
+import WaitingDiv from '../../components/waiting'
+import ErrorDiv from '../error/error'
+
+export default class Ontologies extends Component {
+ constructor (props) {
+ super(props)
+ this.utils = new Utils()
+ this.state = {
+ error: false,
+ errorMessage: '',
+ ontologyError: false,
+ ontologyErrorMessage: '',
+ newontologyError: false,
+ newontologyErrorMessage: '',
+ ontologies: [],
+ datasets: [],
+ name: "",
+ uri: "",
+ shortName: "",
+ type: "local",
+ datasetId: "",
+ labelUri: "rdfs:label",
+ ontologiesSelected: []
+ }
+ this.handleChangeValue = this.handleChangeValue.bind(this)
+ this.handleAddOntology = this.handleAddOntology.bind(this)
+ this.deleteSelectedOntologies = this.deleteSelectedOntologies.bind(this)
+ this.handleOntologySelection = this.handleOntologySelection.bind(this)
+ this.handleOntologySelectionAll = this.handleOntologySelectionAll.bind(this)
+ this.cancelRequest
+ }
+
+ isOntologiesDisabled () {
+ return this.state.ontologiesSelected.length == 0
+ }
+
+ cleanupOntologies (newOntologies) {
+ let cleanOntologies = []
+ newOntologies.map(onto => {
+ cleanOntologies.push({
+ id:onto.id,
+ name:onto.name,
+ uri:onto.uri,
+ short_name: onto.short_name,
+ type: onto.type
+ })
+ })
+ return cleanOntologies
+ }
+
+ deleteSelectedOntologies () {
+ let requestUrl = '/api/admin/delete_ontologies'
+ let data = {
+ ontologiesIdToDelete: this.state.ontologiesSelected
+ }
+
+ axios.post(requestUrl, data, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
+ .then(response => {
+ console.log(requestUrl, response.data)
+ this.setState({
+ ontologies: response.data.ontologies,
+ ontologiesSelected: [],
+ })
+ this.props.setStateNavbar({
+ config: update(this.props.config, {ontologies: {$set: this.cleanupOntologies(response.data.ontologies)}})
+ })
+ })
+ }
+
+ handleChangeValue (event) {
+ let data = {}
+ data[event.target.id] = event.target.value
+ this.setState(data)
+ }
+
+ validateOntologyForm () {
+ return (
+ this.state.name.length > 0 &&
+ this.state.uri.length > 0 &&
+ this.state.shortName.length > 0 &&
+ this.state.datasetId.length > 0 &&
+ this.state.labelUri.length > 0
+ )
+ }
+
+ handleOntologySelection (row, isSelect) {
+ if (isSelect) {
+ this.setState({
+ ontologiesSelected: [...this.state.ontologiesSelected, row.id]
+ })
+ } else {
+ this.setState({
+ ontologiesSelected: this.state.ontologiesSelected.filter(x => x !== row.id)
+ })
+ }
+ }
+
+ handleOntologySelectionAll (isSelect, rows) {
+ const ontologies = rows.map(r => r.id)
+ if (isSelect) {
+ this.setState({
+ ontologiesSelected: ontologies
+ })
+ } else {
+ this.setState({
+ ontologiesSelected: []
+ })
+ }
+ }
+
+ handleAddOntology(event) {
+
+ let requestUrl = "/api/admin/addontology"
+ let data = {
+ name: this.state.name,
+ uri: this.state.uri,
+ shortName: this.state.shortName,
+ type: this.state.type,
+ datasetId: this.state.datasetId,
+ labelUri: this.state.labelUri
+ }
+
+ axios.post(requestUrl, data, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
+ .then(response => {
+ this.setState({
+ newontologyError: response.data.error,
+ newontologyErrorMessage: [response.data.errorMessage],
+ ontologies: response.data.ontologies,
+ newontologyStatus: response.status,
+ name: "",
+ uri: "",
+ shortName: "",
+ type: "local",
+ labelUri: "rdfs:label"
+ })
+ this.props.setStateNavbar({
+ config: update(this.props.config, {ontologies: {$set: this.cleanupOntologies(response.data.ontologies)}})
+ })
+ })
+ .catch(error => {
+ console.log(error, error.response.data.errorMessage)
+ this.setState({
+ newontologyError: true,
+ newontologyErrorMessage: [error.response.data.errorMessage],
+ newontologyStatus: error.response.status,
+ })
+ })
+ event.preventDefault()
+ }
+
+ componentDidMount () {
+ if (!this.props.waitForStart) {
+ this.loadOntologies()
+ this.loadDatasets()
+ }
+ }
+
+ loadOntologies() {
+ let requestUrl = '/api/admin/getontologies'
+ axios.get(requestUrl, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
+ .then(response => {
+ this.setState({
+ ontologiesLoading: false,
+ ontologies: response.data.ontologies
+ })
+ })
+ .catch(error => {
+ console.log(error, error.response.data.errorMessage)
+ this.setState({
+ ontologyError: true,
+ ontologyErrorMessage: error.response.data.errorMessage,
+ ontologyStatus: error.response.status,
+ success: !error.response.data.error
+ })
+ })
+ }
+
+ loadDatasets() {
+ let requestUrl = '/api/datasets'
+ axios.get(requestUrl, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
+ .then(response => {
+ this.setState({
+ datasets: response.data.datasets
+ })
+ })
+ .catch(error => {
+ console.log(error, error.response.data.errorMessage)
+ this.setState({
+ success: !error.response.data.error
+ })
+ })
+ }
+
+ componentWillUnmount () {
+ if (!this.props.waitForStart) {
+ this.cancelRequest()
+ }
+ }
+
+ render () {
+
+ if (!this.props.waitForStart && !this.props.config.logged) {
+ return
+ }
+ if (!this.props.waitForStart && this.props.config.user.admin != 1) {
+ return
+ }
+
+ if (this.props.waitForStart) {
+ return
+ }
+
+ let ontologiesColumns = [{
+ editable: false,
+ dataField: 'name',
+ text: 'Name',
+ sort: true
+ }, {
+ editable: false,
+ dataField: 'short_name',
+ text: 'Short name',
+ sort: true
+ }, {
+ editable: false,
+ dataField: 'uri',
+ text: 'Uri',
+ sort: true
+ }, {
+ editable: false,
+ dataField: 'dataset_name',
+ text: 'Dataset',
+ sort: true
+ }, {
+ editable: false,
+ dataField: 'label_uri',
+ text: 'Label uri',
+ sort: true
+ }, {
+ editable: false,
+ dataField: 'type',
+ text: 'Autocomplete type',
+ sort: true
+ }
+
+ ]
+
+ let ontologiesDefaultSorted = [{
+ dataField: 'short_name',
+ order: 'asc'
+ }]
+
+ let ontologiesSelectRow = {
+ mode: 'checkbox',
+ clickToSelect: false,
+ selected: this.state.ontologiesSelected,
+ onSelect: this.handleOntologySelection,
+ onSelectAll: this.handleOntologySelectionAll
+ }
+
+ let ontologiesNoDataIndication = 'No ontologies'
+ if (this.state.ontologiesLoading) {
+ ontologiesNoDataIndication =
+ }
+
+ return (
+
+
Admin
+
+
Add a ontology
+
+
+
+
+
+
+
+
+
+
+
Delete
+
+
+ )
+ }
+}
+
+Ontologies.propTypes = {
+ setStateNavbar: PropTypes.func,
+ waitForStart: PropTypes.bool,
+ config: PropTypes.object
+}
diff --git a/askomics/react/src/routes/admin/prefixes.jsx b/askomics/react/src/routes/admin/prefixes.jsx
new file mode 100644
index 00000000..8be17418
--- /dev/null
+++ b/askomics/react/src/routes/admin/prefixes.jsx
@@ -0,0 +1,257 @@
+import React, { Component } from 'react'
+import axios from 'axios'
+import {Button, Form, FormGroup, Label, Input, Alert, Row, Col, CustomInput } from 'reactstrap'
+import BootstrapTable from 'react-bootstrap-table-next'
+import paginationFactory from 'react-bootstrap-table2-paginator'
+import PropTypes from 'prop-types'
+import Utils from '../../classes/utils'
+import { Redirect } from 'react-router-dom'
+import WaitingDiv from '../../components/waiting'
+import ErrorDiv from '../error/error'
+
+export default class Prefixes extends Component {
+ constructor (props) {
+ super(props)
+ this.utils = new Utils()
+ this.state = {
+ error: false,
+ errorMessage: '',
+ prefixError: false,
+ prefixErrorMessage: '',
+ newprefixError: false,
+ newprefixErrorMessage: '',
+ prefixes: [],
+ prefix: "",
+ namespace: "",
+ prefixesSelected: []
+ }
+ this.handleChangePrefix = this.handleChangePrefix.bind(this)
+ this.handleChangeNamespace = this.handleChangeNamespace.bind(this)
+ this.handleAddPrefix = this.handleAddPrefix.bind(this)
+ this.deleteSelectedPrefixes = this.deleteSelectedPrefixes.bind(this)
+ this.handlePrefixSelection = this.handlePrefixSelection.bind(this)
+ this.handlePrefixSelectionAll = this.handlePrefixSelectionAll.bind(this)
+ this.cancelRequest
+ }
+
+ isPrefixesDisabled () {
+ return this.state.prefixesSelected.length == 0
+ }
+
+ deleteSelectedPrefixes () {
+ let requestUrl = '/api/admin/delete_prefixes'
+ let data = {
+ prefixesIdToDelete: this.state.prefixesSelected
+ }
+ axios.post(requestUrl, data, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
+ .then(response => {
+ console.log(requestUrl, response.data)
+ this.setState({
+ prefixes: response.data.prefixes,
+ prefixesSelected: [],
+ })
+ })
+ }
+
+ handleChangePrefix (event) {
+ this.setState({
+ prefix: event.target.value
+ })
+ }
+
+ handleChangeNamespace (event) {
+ this.setState({
+ namespace: event.target.value
+ })
+ }
+
+ validatePrefixForm () {
+ return (
+ this.state.prefix.length > 0 &&
+ this.state.namespace.length > 0
+ )
+ }
+
+ handlePrefixSelection (row, isSelect) {
+ if (isSelect) {
+ this.setState({
+ prefixesSelected: [...this.state.prefixesSelected, row.id]
+ })
+ } else {
+ this.setState({
+ prefixesSelected: this.state.prefixesSelected.filter(x => x !== row.id)
+ })
+ }
+ }
+
+ handlePrefixSelectionAll (isSelect, rows) {
+ const prefixes = rows.map(r => r.id)
+ if (isSelect) {
+ this.setState({
+ prefixesSelected: prefixes
+ })
+ } else {
+ this.setState({
+ prefixesSelected: []
+ })
+ }
+ }
+
+ handleAddPrefix(event) {
+
+ let requestUrl = "/api/admin/addprefix"
+ let data = {
+ prefix: this.state.prefix,
+ namespace: this.state.namespace,
+ }
+
+ axios.post(requestUrl, data, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
+ .then(response => {
+ console.log(requestUrl, response.data)
+ this.setState({
+ newprefixError: response.data.error,
+ newprefixErrorMessage: response.data.errorMessage,
+ prefixes: response.data.prefixes,
+ newprefixStatus: response.status,
+ })
+ })
+ .catch(error => {
+ console.log(error, error.response.data.errorMessage)
+ this.setState({
+ newprefixError: true,
+ newprefixErrorMessage: error.response.data.errorMessage,
+ newprefixStatus: error.response.status,
+ })
+ })
+ event.preventDefault()
+ }
+
+ componentDidMount () {
+ if (!this.props.waitForStart) {
+ this.loadPrefixes()
+ }
+ }
+
+ loadPrefixes() {
+ let requestUrl = '/api/admin/getprefixes'
+ axios.get(requestUrl, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
+ .then(response => {
+ this.setState({
+ prefixesLoading: false,
+ prefixes: response.data.prefixes
+ })
+ })
+ .catch(error => {
+ console.log(error, error.response.data.errorMessage)
+ this.setState({
+ prefixError: true,
+ prefixErrorMessage: error.response.data.errorMessage,
+ prefixStatus: error.response.status,
+ success: !error.response.data.error
+ })
+ })
+ }
+
+ componentWillUnmount () {
+ if (!this.props.waitForStart) {
+ this.cancelRequest()
+ }
+ }
+
+ render () {
+
+ if (!this.props.waitForStart && !this.props.config.logged) {
+ return
+ }
+ if (!this.props.waitForStart && this.props.config.user.admin != 1) {
+ return
+ }
+
+ if (this.props.waitForStart) {
+ return
+ }
+
+ let prefixesColumns = [{
+ editable: false,
+ dataField: 'prefix',
+ text: 'Prefix',
+ sort: true
+ }, {
+ editable: false,
+ dataField: 'namespace',
+ text: 'Namespace',
+ sort: true
+ }]
+
+ let prefixesDefaultSorted = [{
+ dataField: 'prefix',
+ order: 'asc'
+ }]
+
+ let prefixesSelectRow = {
+ mode: 'checkbox',
+ clickToSelect: false,
+ selected: this.state.prefixesSelected,
+ onSelect: this.handlePrefixSelection,
+ onSelectAll: this.handlePrefixSelectionAll
+ }
+
+ let prefixesNoDataIndication = 'No custom prefixes'
+ if (this.state.prefixesLoading) {
+ prefixesNoDataIndication =
+ }
+
+ return (
+
+
Admin
+
+
Add a prefix
+
+
+
+
+
+
+
+
+
+
+
Delete
+
+
+ )
+ }
+}
+
+Prefixes.propTypes = {
+ waitForStart: PropTypes.bool,
+ config: PropTypes.object
+}
diff --git a/askomics/react/src/routes/ask/ask.jsx b/askomics/react/src/routes/ask/ask.jsx
index e788fef5..991ed46a 100644
--- a/askomics/react/src/routes/ask/ask.jsx
+++ b/askomics/react/src/routes/ask/ask.jsx
@@ -29,7 +29,8 @@ export default class Ask extends Component {
dropdownOpen: false,
selectedEndpoint: [],
frontMessage: "",
- redirectFormBuilder: false
+ redirectFormBuilder: false,
+ console_enabled: false
}
this.utils = new Utils()
this.cancelRequest
@@ -155,6 +156,7 @@ export default class Ask extends Component {
graphs: response.data.graphs,
endpoints_sparql: response.data.endpoints,
diskSpace: response.data.diskSpace,
+ console_enabled: response.data.console_enabled
})
}
})
@@ -276,7 +278,8 @@ export default class Ask extends Component {
graphs: this.state.graphs,
endpoints: this.state.endpoints_sparql,
diskSpace: this.state.diskSpace,
- config: this.props.config
+ config: this.props.config,
+ console_enabled: this.state.console_enabled
}
}} />
}
diff --git a/askomics/react/src/routes/data/data.jsx b/askomics/react/src/routes/data/data.jsx
index 1b23cbc2..18b41092 100644
--- a/askomics/react/src/routes/data/data.jsx
+++ b/askomics/react/src/routes/data/data.jsx
@@ -16,7 +16,7 @@ class Data extends Component {
constructor (props) {
super(props)
this.utils = new Utils()
- this.state = {
+ this.state = {
isLoading: true,
error: false,
errorMessage: '',
@@ -32,7 +32,7 @@ class Data extends Component {
loadData() {
let uri = this.props.match.params.uri;
- let requestUrl = '/api/data/' + uri
+ let requestUrl = '/api/data/' + uri
axios.get(requestUrl, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
.then(response => {
console.log(requestUrl, response.data)
@@ -61,7 +61,7 @@ class Data extends Component {
let uri = this.props.match.params.uri;
let columns = [{
- dataField: 'predicat',
+ dataField: 'predicate',
text: 'Property',
sort: true,
formatter: (cell, row) => {
@@ -79,7 +79,7 @@ class Data extends Component {
if (cell.startsWith(this.props.config.namespaceInternal)){
return this.utils.splitUrl(cell)
} else {
- return {this.utils.splitUrl(cell)}
+ return {this.utils.splitUrl(cell)}
}
}
return cell
@@ -90,7 +90,7 @@ class Data extends Component {
return (
Information about uri {uri}
-
+
-
+
)
@@ -209,4 +209,4 @@ DatasetsTable.propTypes = {
waiting: PropTypes.bool,
datasets: PropTypes.object,
config: PropTypes.object
-}
\ No newline at end of file
+}
diff --git a/askomics/react/src/routes/form/attribute.jsx b/askomics/react/src/routes/form/attribute.jsx
index 7b59e403..7581a5da 100644
--- a/askomics/react/src/routes/form/attribute.jsx
+++ b/askomics/react/src/routes/form/attribute.jsx
@@ -32,6 +32,17 @@ export default class AttributeBox extends Component {
this.handleDateFilter = this.props.handleDateFilter.bind(this)
}
+ subNums (id) {
+ let newStr = ""
+ let oldStr = id.toString()
+ let arrayString = [...oldStr]
+ arrayString.forEach(char => {
+ let code = char.charCodeAt()
+ newStr += String.fromCharCode(code + 8272)
+ })
+ return newStr
+ }
+
renderLinker () {
let options = []
@@ -68,58 +79,6 @@ export default class AttributeBox extends Component {
}
}
- renderUri () {
- let eyeIcon = 'attr-icon fas fa-eye-slash inactive'
- if (this.props.attribute.visible) {
- eyeIcon = 'attr-icon fas fa-eye'
- }
-
- let linkIcon = 'attr-icon fas fa-unlink inactive'
- if (this.props.attribute.linked) {
- linkIcon = 'attr-icon fas fa-link'
- }
-
- let selected_sign = {
- '=': !this.props.attribute.negative,
- "≠": this.props.attribute.negative
- }
-
- let form
-
- if (this.props.attribute.linked) {
- form = this.renderLinker()
- } else {
- form = (
-
- )
- }
-
- return (
-
-
{this.props.attribute.displayLabel}
-
-
-
-
- {form}
-
- )
- }
-
renderText () {
let eyeIcon = 'attr-icon fas fa-eye-slash inactive'
@@ -186,11 +145,10 @@ export default class AttributeBox extends Component {
return (
-
{this.props.attribute.displayLabel}
+
{this.props.attribute.displayLabel ? this.props.attribute.displayLabel : this.props.attribute.label}
-
- {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : }
-
+ {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : }
+
{form}
@@ -256,11 +214,10 @@ export default class AttributeBox extends Component {
return (
-
{this.props.attribute.displayLabel}
+
{this.props.attribute.displayLabel ? this.props.attribute.displayLabel : this.props.attribute.label}
-
-
-
+
+
{form}
@@ -308,12 +265,11 @@ export default class AttributeBox extends Component {
return (
-
{this.props.attribute.displayLabel}
+
{this.props.attribute.displayLabel ? this.props.attribute.displayLabel : this.props.attribute.label}
-
-
-
-
+
+
+
{form}
@@ -354,11 +310,10 @@ export default class AttributeBox extends Component {
return (
-
{this.props.attribute.displayLabel}
+
{this.props.attribute.displayLabel ? this.props.attribute.displayLabel : this.props.attribute.label}
-
-
-
+
+
{form}
@@ -432,11 +387,10 @@ export default class AttributeBox extends Component {
return (
-
{this.props.attribute.displayLabel}
+
{this.props.attribute.displayLabel ? this.props.attribute.displayLabel : this.props.attribute.label}
-
-
-
+
+
{form}
diff --git a/askomics/react/src/routes/form/query.jsx b/askomics/react/src/routes/form/query.jsx
index 3b0fe52d..c3ce9c20 100644
--- a/askomics/react/src/routes/form/query.jsx
+++ b/askomics/react/src/routes/form/query.jsx
@@ -5,6 +5,7 @@ import { Redirect } from 'react-router-dom'
import ErrorDiv from '../error/error'
import WaitingDiv from '../../components/waiting'
import update from 'react-addons-update'
+import ReactTooltip from "react-tooltip";
import AttributeBox from './attribute'
import Entity from './entity'
import ResultsTable from '../sparql/resultstable'
@@ -55,7 +56,7 @@ export default class FormQuery extends Component {
this.handlePreview = this.handlePreview.bind(this)
this.handleQuery = this.handleQuery.bind(this)
-
+
}
subNums (id) {
@@ -275,6 +276,7 @@ export default class FormQuery extends Component {
saveIcon: "play",
waiting: waiting
})
+ ReactTooltip.rebuild();
}
// Preview results and Launch query buttons -------
@@ -383,7 +385,6 @@ export default class FormQuery extends Component {
})
}).then(response => {
this.graphState = this.props.location.state.graphState
- console.log(this.props.location.state.graphState)
this.updateGraphState()
this.setState({ waiting: false })
})
@@ -435,12 +436,21 @@ export default class FormQuery extends Component {
let previewButton
let launchQueryButton
let entityMap = new Map()
+ let tooltips = (
+
+ Mark attribute as a form attribute
+ Link this attribute to another
+ Show all values, including empty values.
+ Exclude categories, instead of including
+ Display attribute value in the results
+
+ )
if (!this.state.waiting) {
this.state.graphState.attr.forEach(attribute => {
if (attribute.form) {
if (! entityMap.has(attribute.nodeId)){
- entityMap.set(attribute.nodeId, {entity_label: attribute.entityDisplayLabel, attributes:[]})
+ entityMap.set(attribute.nodeId, {entity_label: attribute.entityDisplayLabel ? attribute.entityDisplayLabel : attribute.entityLabel, attributes:[]})
}
entityMap.get(attribute.nodeId).attributes.push(
{Entities}
+ {tooltips}
diff --git a/askomics/react/src/routes/form_edit/attribute.jsx b/askomics/react/src/routes/form_edit/attribute.jsx
index f7c95455..eb85d041 100644
--- a/askomics/react/src/routes/form_edit/attribute.jsx
+++ b/askomics/react/src/routes/form_edit/attribute.jsx
@@ -32,6 +32,17 @@ export default class AttributeBox extends Component {
this.handleDateFilter = this.props.handleDateFilter.bind(this)
}
+ subNums (id) {
+ let newStr = ""
+ let oldStr = id.toString()
+ let arrayString = [...oldStr]
+ arrayString.forEach(char => {
+ let code = char.charCodeAt()
+ newStr += String.fromCharCode(code + 8272)
+ })
+ return newStr
+ }
+
renderLinker () {
let options = []
@@ -68,58 +79,6 @@ export default class AttributeBox extends Component {
}
}
- renderUri () {
- let eyeIcon = 'attr-icon fas fa-eye-slash inactive'
- if (this.props.attribute.visible) {
- eyeIcon = 'attr-icon fas fa-eye'
- }
-
- let linkIcon = 'attr-icon fas fa-unlink inactive'
- if (this.props.attribute.linked) {
- linkIcon = 'attr-icon fas fa-link'
- }
-
- let selected_sign = {
- '=': !this.props.attribute.negative,
- "≠": this.props.attribute.negative
- }
-
- let form
-
- if (this.props.attribute.linked) {
- form = this.renderLinker()
- } else {
- form = (
-
- )
- }
-
- return (
-
- )
- }
-
renderText () {
let eyeIcon = 'attr-icon fas fa-eye-slash inactive'
@@ -188,11 +147,10 @@ export default class AttributeBox extends Component {
return (
@@ -258,11 +216,10 @@ export default class AttributeBox extends Component {
return (
@@ -310,12 +267,11 @@ export default class AttributeBox extends Component {
return (
@@ -356,11 +312,10 @@ export default class AttributeBox extends Component {
return (
@@ -434,11 +389,10 @@ export default class AttributeBox extends Component {
return (
diff --git a/askomics/react/src/routes/form_edit/query.jsx b/askomics/react/src/routes/form_edit/query.jsx
index 88aeeb44..a77207ac 100644
--- a/askomics/react/src/routes/form_edit/query.jsx
+++ b/askomics/react/src/routes/form_edit/query.jsx
@@ -5,6 +5,7 @@ import { Redirect } from 'react-router-dom'
import ErrorDiv from '../error/error'
import WaitingDiv from '../../components/waiting'
import update from 'react-addons-update'
+import ReactTooltip from "react-tooltip";
import AttributeBox from './attribute'
import Entity from './entity'
import ResultsTable from '../sparql/resultstable'
@@ -267,6 +268,7 @@ export default class FormEditQuery extends Component {
saveIcon: "play",
waiting: waiting
})
+ ReactTooltip.rebuild();
}
// Preview results and Launch query buttons -------
@@ -385,12 +387,21 @@ export default class FormEditQuery extends Component {
let Entities = []
let previewButton
let entityMap = new Map()
+ let tooltips = (
+
+ Mark attribute as a form attribute
+ Link this attribute to another
+ Show all values, including empty values.
+ Exclude categories, instead of including
+ Display attribute value in the results
+
+ )
if (!this.state.waiting) {
this.state.graphState.attr.forEach(attribute => {
if (attribute.form) {
if (! entityMap.has(attribute.nodeId)){
- entityMap.set(attribute.nodeId, {entity_label: attribute.entityDisplayLabel, attributes:[]})
+ entityMap.set(attribute.nodeId, {entity_label: attribute.entityDisplayLabel ? attribute.entityDisplayLabel : attribute.entityLabel, attributes:[]})
}
entityMap.get(attribute.nodeId).attributes.push(
Save
// preview
@@ -445,7 +456,7 @@ export default class FormEditQuery extends Component {
return (
{redirectLogin}
-
Query Builder
+
Form editor
@@ -453,6 +464,7 @@ export default class FormEditQuery extends Component {
{Entities}
+ {tooltips}
diff --git a/askomics/react/src/routes/integration/advancedoptions.jsx b/askomics/react/src/routes/integration/advancedoptions.jsx
index 4a6c8988..d54375bf 100644
--- a/askomics/react/src/routes/integration/advancedoptions.jsx
+++ b/askomics/react/src/routes/integration/advancedoptions.jsx
@@ -41,6 +41,12 @@ export default class AdvancedOptions extends Component {
+
+ Distant graph
+
+
+
+
@@ -55,5 +61,7 @@ AdvancedOptions.propTypes = {
hideCustomUri: PropTypes.bool,
customUri: PropTypes.string,
hideDistantEndpoint: PropTypes.bool,
- externalEndpoint: PropTypes.string
+ externalEndpoint: PropTypes.string,
+ handleChangeExternalGraph: PropTypes.function,
+ externalGraph: PropTypes.string
}
diff --git a/askomics/react/src/routes/integration/bedpreview.jsx b/askomics/react/src/routes/integration/bedpreview.jsx
index 803ba86c..ad3f7269 100644
--- a/askomics/react/src/routes/integration/bedpreview.jsx
+++ b/askomics/react/src/routes/integration/bedpreview.jsx
@@ -21,7 +21,8 @@ export default class BedPreview extends Component {
externalEndpoint: "",
error: false,
errorMessage: null,
- status: null
+ status: null,
+ externalGraph: ""
}
this.cancelRequest
this.integrate = this.integrate.bind(this)
@@ -81,6 +82,14 @@ export default class BedPreview extends Component {
})
}
+ handleChangeExternalGraph (event) {
+ this.setState({
+ externalGraph: event.target.value,
+ publicTick: false,
+ privateTick: false
+ })
+ }
+
render () {
let privateIcon =
@@ -91,9 +100,12 @@ export default class BedPreview extends Component {
if (this.state.publicTick) {
publicIcon =
}
-
+ let privateButton
+ if (this.props.config.user.admin || !this.props.config.singleTenant){
+ privateButton = {privateIcon} Integrate (private dataset)
+ }
let publicButton
- if (this.props.config.user.admin) {
+ if (this.props.config.user.admin || this.props.config.singleTenant) {
publicButton = {publicIcon} Integrate (public dataset)
}
@@ -116,14 +128,17 @@ export default class BedPreview extends Component {
this.handleChangeUri(p)}
handleChangeEndpoint={p => this.handleChangeEndpoint(p)}
+ handleChangeExternalGraph={p => this.handleChangeExternalGraph(p)}
+ externalGraph={this.state.externalGraph}
customUri={this.state.customUri}
/>
- {privateIcon} Integrate (private dataset)
+ {privateButton}
{publicButton}
diff --git a/askomics/react/src/routes/integration/csvtable.jsx b/askomics/react/src/routes/integration/csvtable.jsx
index f8c0547e..dddd55ba 100644
--- a/askomics/react/src/routes/integration/csvtable.jsx
+++ b/askomics/react/src/routes/integration/csvtable.jsx
@@ -26,7 +26,8 @@ export default class CsvTable extends Component {
externalEndpoint: "",
error: false,
errorMessage: null,
- status: null
+ status: null,
+ externalGraph: ""
}
this.cancelRequest
this.headerFormatter = this.headerFormatter.bind(this)
@@ -91,6 +92,51 @@ export default class CsvTable extends Component {
)
}
+ let ontoInput
+
+ if (this.props.ontologies.length > 0){
+ ontoInput = (
+
+ {this.props.ontologies.map(onto => {
+ return {onto.name}
+ })}
+
+ )
+ }
+
+ if (colIndex == 1) {
+ return (
+
+
+ {colInput}
+
+
+ Entity label
+
+
+ Numeric
+ Text
+ Category
+ Boolean
+ Date
+
+
+ Reference
+ Strand
+ Start
+ End
+
+
+ Directed
+ Symetric
+
+ {ontoInput}
+
+
+
+ )
+ }
+
return (
@@ -113,6 +159,7 @@ export default class CsvTable extends Component {
Directed
Symetric
+ {ontoInput}
@@ -167,6 +214,14 @@ export default class CsvTable extends Component {
})
}
+ handleChangeExternalGraph (event) {
+ this.setState({
+ externalGraph: event.target.value,
+ publicTick: false,
+ privateTick: false
+ })
+ }
+
toggleHeaderForm(event) {
this.setState({
header: update(this.state.header, { [event.target.id]: { input: { $set: true } } })
@@ -192,7 +247,7 @@ export default class CsvTable extends Component {
formatter: (cell, row) => {
let text = row[this.state.header[index]["name"]]
if (this.utils.isUrl(text)) {
- return
{this.utils.truncate(this.utils.splitUrl(text), 25)}
+ return
{this.utils.truncate(this.utils.splitUrl(text), 25)}
} else {
return this.utils.truncate(text, 25)
}
@@ -208,9 +263,12 @@ export default class CsvTable extends Component {
if (this.state.publicTick) {
publicIcon =
}
-
+ let privateButton
+ if (this.props.config.user.admin || !this.props.config.singleTenant){
+ privateButton =
{privateIcon} Integrate (private dataset)
+ }
let publicButton
- if (this.props.config.user.admin) {
+ if (this.props.config.user.admin || this.props.config.singleTenant) {
publicButton =
{publicIcon} Integrate (public dataset)
}
@@ -236,14 +294,17 @@ export default class CsvTable extends Component {
this.handleChangeUri(p)}
handleChangeEndpoint={p => this.handleChangeEndpoint(p)}
+ handleChangeExternalGraph={p => this.handleChangeExternalGraph(p)}
+ externalGraph={this.state.externalGraph}
customUri={this.state.customUri}
/>
- {privateIcon} Integrate (private dataset)
+ {privateButton}
{publicButton}
@@ -264,5 +325,6 @@ export default class CsvTable extends Component {
CsvTable.propTypes = {
file: PropTypes.object,
- config: PropTypes.object
+ config: PropTypes.object,
+ ontologies: PropTypes.array
}
diff --git a/askomics/react/src/routes/integration/gffpreview.jsx b/askomics/react/src/routes/integration/gffpreview.jsx
index 077ccf90..ecff25c9 100644
--- a/askomics/react/src/routes/integration/gffpreview.jsx
+++ b/askomics/react/src/routes/integration/gffpreview.jsx
@@ -18,7 +18,8 @@ export default class GffPreview extends Component {
publicTick: false,
privateTick: false,
customUri: "",
- externalEndpoint: ""
+ externalEndpoint: "",
+ externalGraph: ""
}
this.cancelRequest
this.integrate = this.integrate.bind(this)
@@ -90,6 +91,22 @@ export default class GffPreview extends Component {
})
}
+ handleChangeRemoteGraph (event) {
+ this.setState({
+ remoteGraph: event.target.value,
+ publicTick: false,
+ privateTick: false
+ })
+ }
+
+ handleChangeExternalGraph (event) {
+ this.setState({
+ externalGraph: event.target.value,
+ publicTick: false,
+ privateTick: false
+ })
+ }
+
render () {
let privateIcon =
@@ -100,8 +117,12 @@ export default class GffPreview extends Component {
if (this.state.publicTick) {
publicIcon =
}
+ let privateButton
+ if (this.props.config.user.admin || !this.props.config.singleTenant){
+ privateButton = {privateIcon} Integrate (private dataset)
+ }
let publicButton
- if (this.props.config.user.admin) {
+ if (this.props.config.user.admin || this.props.config.singleTenant) {
publicButton = {publicIcon} Integrate (public dataset)
}
@@ -122,14 +143,17 @@ export default class GffPreview extends Component {
this.handleChangeUri(p)}
handleChangeEndpoint={p => this.handleChangeEndpoint(p)}
+ handleChangeExternalGraph={p => this.handleChangeExternalGraph(p)}
+ externalGraph={this.state.externalGraph}
customUri={this.state.customUri}
/>
- {privateIcon} Integrate (private dataset)
+ {privateButton}
{publicButton}
diff --git a/askomics/react/src/routes/integration/integration.jsx b/askomics/react/src/routes/integration/integration.jsx
index ada6f4af..7d3f50f7 100644
--- a/askomics/react/src/routes/integration/integration.jsx
+++ b/askomics/react/src/routes/integration/integration.jsx
@@ -19,7 +19,8 @@ export default class Integration extends Component {
errorMessage: null,
config: this.props.location.state.config,
filesId: this.props.location.state.filesId,
- previewFiles: []
+ previewFiles: [],
+ ontologies: this.props.location.state.config.ontologies
}
this.cancelRequest
}
@@ -74,7 +75,7 @@ export default class Integration extends Component {
this.state.previewFiles.map(file => {
console.log(file)
if (file.type == 'csv/tsv') {
- return
+ return
}
if (["rdf/ttl", "rdf/xml", "rdf/nt"].includes(file.type)) {
return
diff --git a/askomics/react/src/routes/integration/rdfpreview.jsx b/askomics/react/src/routes/integration/rdfpreview.jsx
index 4d62c6e5..654747b4 100644
--- a/askomics/react/src/routes/integration/rdfpreview.jsx
+++ b/askomics/react/src/routes/integration/rdfpreview.jsx
@@ -24,6 +24,7 @@ export default class RdfPreview extends Component {
privateTick: false,
customUri: "",
externalEndpoint: props.file.data.location ? props.file.data.location : "",
+ externalGraph: props.file.data.remote_graph ? props.file.data.remote_graph : "",
error: false,
errorMessage: null,
status: null
@@ -49,7 +50,8 @@ export default class RdfPreview extends Component {
public: event.target.value == 'public',
type: this.props.file.type,
customUri: this.state.customUri,
- externalEndpoint: this.state.externalEndpoint
+ externalEndpoint: this.state.externalEndpoint,
+ externalGraph: this.state.externalGraph
}
axios.post(requestUrl, data, { baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) })
.then(response => {
@@ -85,6 +87,14 @@ export default class RdfPreview extends Component {
})
}
+ handleChangeExternalGraph (event) {
+ this.setState({
+ externalGraph: event.target.value,
+ publicTick: false,
+ privateTick: false
+ })
+ }
+
guess_mode(type) {
if (type == "rdf/ttl") {
return "turtle"
@@ -107,9 +117,12 @@ export default class RdfPreview extends Component {
if (this.state.publicTick) {
publicIcon =
}
-
+ let privateButton
+ if (this.props.config.user.admin || !this.props.config.singleTenant){
+ privateButton =
{privateIcon} Integrate (private dataset)
+ }
let publicButton
- if (this.props.config.user.admin) {
+ if (this.props.config.user.admin || this.props.config.singleTenant) {
publicButton =
{publicIcon} Integrate (public dataset)
}
@@ -139,12 +152,14 @@ export default class RdfPreview extends Component {
config={this.props.config}
handleChangeEndpoint={p => this.handleChangeEndpoint(p)}
externalEndpoint={this.state.externalEndpoint}
+ handleChangeExternalGraph={p => this.handleChangeExternalGraph(p)}
+ externalGraph={this.state.externalGraph}
handleChangeUri={p => this.handleChangeUri(p)}
/>
- {privateIcon} Integrate (private dataset)
+ {privateButton}
{publicButton}
diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx
index c26e8d4a..7f37204f 100644
--- a/askomics/react/src/routes/query/attribute.jsx
+++ b/askomics/react/src/routes/query/attribute.jsx
@@ -3,12 +3,14 @@ import axios from 'axios'
import { Input, FormGroup, CustomInput, FormFeedback } from 'reactstrap'
import { Redirect } from 'react-router-dom'
import DatePicker from "react-datepicker";
+import ReactTooltip from "react-tooltip";
import ErrorDiv from '../error/error'
import WaitingDiv from '../../components/waiting'
import update from 'react-addons-update'
import Visualization from './visualization'
import PropTypes from 'prop-types'
import Utils from '../../classes/utils'
+import Autocomplete from '../../components/autocomplete'
export default class AttributeBox extends Component {
constructor (props) {
@@ -32,6 +34,7 @@ export default class AttributeBox extends Component {
this.toggleAddNumFilter = this.props.toggleAddNumFilter.bind(this)
this.toggleAddDateFilter = this.props.toggleAddDateFilter.bind(this)
this.handleDateFilter = this.props.handleDateFilter.bind(this)
+ this.cancelRequest
}
subNums (id) {
@@ -45,6 +48,13 @@ export default class AttributeBox extends Component {
return newStr
}
+
+ isRegisteredOnto () {
+ return this.props.config.ontologies.some(onto => {
+ return (onto.uri == this.props.entityUri && onto.type != "none")
+ })
+ }
+
renderLinker () {
let options = []
@@ -81,65 +91,6 @@ export default class AttributeBox extends Component {
}
}
- renderUri () {
-
- let formIcon = 'attr-icon fas fa-bookmark inactive'
- if (this.props.attribute.form) {
- formIcon = 'attr-icon fas fa-bookmark '
- }
-
- let eyeIcon = 'attr-icon fas fa-eye-slash inactive'
- if (this.props.attribute.visible) {
- eyeIcon = 'attr-icon fas fa-eye'
- }
-
- let linkIcon = 'attr-icon fas fa-unlink inactive'
- if (this.props.attribute.linked) {
- linkIcon = 'attr-icon fas fa-link'
- }
-
- let selected_sign = {
- '=': !this.props.attribute.negative,
- "≠": this.props.attribute.negative
- }
-
- let form
-
- if (this.props.attribute.linked) {
- form = this.renderLinker()
- } else {
- form = (
-
- )
- }
-
- return (
-
-
{this.props.attribute.label}
-
- {this.props.config.user.admin ? : } : }
-
-
-
- {form}
-
- )
- }
-
renderText () {
let formIcon = 'attr-icon fas fa-bookmark inactive'
@@ -181,6 +132,36 @@ export default class AttributeBox extends Component {
let form
+ let input
+ let attrIcons
+
+ if (this.props.isOnto){
+ attrIcons = (
+
+
+
+ )
+ if (this.isRegisteredOnto() && this.props.attribute.uri == "rdfs:label"){
+ input = (
+
this.handleFilterValue(p)}/>
+ )
+ } else {
+ input = ( )
+ }
+
+ } else {
+ attrIcons = (
+
+ {this.props.config.user.admin ? : }
+
+ {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : }
+
+
+ )
+ input = ( )
+ }
+
+
if (this.props.attribute.linked) {
form = this.renderLinker()
} else {
@@ -202,7 +183,7 @@ export default class AttributeBox extends Component {
-
+ {input}
@@ -212,12 +193,7 @@ export default class AttributeBox extends Component {
return (
{this.props.attribute.label}
-
- {this.props.config.user.admin ? : }
-
- {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : }
-
-
+ {attrIcons}
{form}
)
@@ -289,10 +265,10 @@ export default class AttributeBox extends Component {
{this.props.attribute.label}
- {this.props.config.user.admin ? : }
-
-
-
+ {this.props.config.user.admin ? : }
+
+
+
{form}
@@ -347,11 +323,11 @@ export default class AttributeBox extends Component {
{this.props.attribute.label}
- {this.props.config.user.admin ? : }
-
-
-
-
+ {this.props.config.user.admin ? : }
+
+
+
+
{form}
@@ -399,10 +375,10 @@ export default class AttributeBox extends Component {
{this.props.attribute.label}
- {this.props.config.user.admin ? : }
-
-
-
+ {this.props.config.user.admin ? : }
+
+
+
{form}
@@ -485,8 +461,8 @@ export default class AttributeBox extends Component {
{this.props.config.user.admin ? : }
-
-
+
+
{form}
@@ -534,5 +510,7 @@ AttributeBox.propTypes = {
handleDateFilter: PropTypes.func,
attribute: PropTypes.object,
graph: PropTypes.object,
- config: PropTypes.object
+ config: PropTypes.object,
+ isOnto: PropTypes.bool,
+ entityUri: PropTypes.string
}
diff --git a/askomics/react/src/routes/query/graphfilters.js b/askomics/react/src/routes/query/graphfilters.js
index 2750c8d7..8dd4f4e0 100644
--- a/askomics/react/src/routes/query/graphfilters.js
+++ b/askomics/react/src/routes/query/graphfilters.js
@@ -47,7 +47,7 @@ export default class GraphFilters extends Component {
-
+
)
@@ -57,6 +57,8 @@ export default class GraphFilters extends Component {
GraphFilters.propTypes = {
graph: PropTypes.object,
current: PropTypes.object,
+ showFaldo: PropTypes.bool,
handleFilterLinks: PropTypes.func,
- handleFilterNodes: PropTypes.func
-}
\ No newline at end of file
+ handleFilterNodes: PropTypes.func,
+ handleFilterFaldo: PropTypes.func
+}
diff --git a/askomics/react/src/routes/query/ontolinkview.jsx b/askomics/react/src/routes/query/ontolinkview.jsx
new file mode 100644
index 00000000..99e5e435
--- /dev/null
+++ b/askomics/react/src/routes/query/ontolinkview.jsx
@@ -0,0 +1,46 @@
+import React, { Component } from 'react'
+import axios from 'axios'
+import { Input, FormGroup, CustomInput, Col, Row, Button } from 'reactstrap'
+import { Redirect } from 'react-router-dom'
+import ErrorDiv from '../error/error'
+import WaitingDiv from '../../components/waiting'
+import update from 'react-addons-update'
+import Visualization from './visualization'
+import PropTypes from 'prop-types'
+
+export default class OntoLinkView extends Component {
+ constructor (props) {
+ super(props)
+ this.handleChangeOntologyType = this.props.handleChangeOntologyType.bind(this)
+ }
+
+
+ render () {
+ return (
+
+
Ontological Relation
+
+
Search on ...
+
+
+
+
+ children of
+ descendants of
+ parents of
+ ancestors of
+
+
+ a term
+
+
+
+
+ )
+ }
+}
+
+OntoLinkView.propTypes = {
+ link: PropTypes.object,
+ handleChangeOntologyType: PropTypes.func,
+}
diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx
index 66624e0b..ac4000de 100644
--- a/askomics/react/src/routes/query/query.jsx
+++ b/askomics/react/src/routes/query/query.jsx
@@ -1,13 +1,15 @@
import React, { Component } from 'react'
import axios from 'axios'
-import { Alert, Button, Row, Col, ButtonGroup, Input, Spinner } from 'reactstrap'
+import { Alert, Button, CustomInput, Row, Col, ButtonGroup, Input, Spinner } from 'reactstrap'
import { Redirect } from 'react-router-dom'
import ErrorDiv from '../error/error'
import WaitingDiv from '../../components/waiting'
import update from 'react-addons-update'
+import ReactTooltip from "react-tooltip";
import Visualization from './visualization'
import AttributeBox from './attribute'
import LinkView from './linkview'
+import OntoLinkView from './ontolinkview'
import GraphFilters from './graphfilters'
import ResultsTable from '../sparql/resultstable'
import PropTypes from 'prop-types'
@@ -40,7 +42,8 @@ export default class Query extends Component {
// Preview icons
disablePreview: false,
- previewIcon: "table"
+ previewIcon: "table",
+ ontologies: this.props.location.state.config.ontologies
}
this.graphState = {
@@ -50,6 +53,8 @@ export default class Query extends Component {
}
this.divHeight = 650
+ this.showFaldo = true;
+
this.idNumber = 0
this.specialNodeIdNumber = 0
@@ -62,6 +67,7 @@ export default class Query extends Component {
this.handleRemoveNode = this.handleRemoveNode.bind(this)
this.handleFilterNodes = this.handleFilterNodes.bind(this)
this.handleFilterLinks = this.handleFilterLinks.bind(this)
+ this.handleFilterFaldo = this.handleFilterFaldo.bind(this)
}
resetIcons() {
@@ -208,6 +214,33 @@ export default class Query extends Component {
})
}
+ isRemoteOnto (currentUri, targetUri) {
+
+ let node = this.state.abstraction.entities.find(entity => {
+ return entity.uri == targetUri
+ })
+
+ if (! node){
+ return false
+ }
+
+ return node.ontology ? currentUri == targetUri ? "endNode" : "node" : false
+ }
+
+ isOntoNode (currentId) {
+
+ return this.graphState.nodes.some(node => {
+ return (node.id == currentId && node.ontology)
+ })
+ }
+
+ isOntoEndNode (currentId) {
+
+ return this.graphState.nodes.some(node => {
+ return (node.id == currentId && node.ontology == "endNode")
+ })
+ }
+
attributeExist (attrUri, nodeId) {
return this.graphState.attr.some(attr => {
return (attr.uri == attrUri && attr.nodeId == nodeId)
@@ -240,6 +273,8 @@ export default class Query extends Component {
let nodeAttributes = []
let isBnode = this.isBnode(nodeId)
+ let isOnto = this.isOntoNode(nodeId)
+
// if bnode without uri, first attribute is visible
let firstAttrVisibleForBnode = isBnode
@@ -255,7 +290,9 @@ export default class Query extends Component {
humanNodeId: this.getHumanIdFromId(nodeId),
uri: 'rdf:type',
label: 'Uri',
+ displayLabel: 'Uri',
entityLabel: this.getLabel(nodeUri),
+ entityDisplayLabel: this.getLabel(nodeUri),
entityUris: [nodeUri, ],
type: 'uri',
faldo: false,
@@ -265,7 +302,8 @@ export default class Query extends Component {
form: false,
negative: false,
linked: false,
- linkedWith: null
+ linkedWith: null,
+ ontology: isOnto
})
}
@@ -278,7 +316,9 @@ export default class Query extends Component {
humanNodeId: this.getHumanIdFromId(nodeId),
uri: 'rdfs:label',
label: 'Label',
+ displayLabel: 'Label',
entityLabel: this.getLabel(nodeUri),
+ entityDisplayLabel: this.getLabel(nodeUri),
entityUris: [nodeUri, ],
type: 'text',
faldo: false,
@@ -474,7 +514,12 @@ export default class Query extends Component {
let specialNodeGroupId = incrementSpecialNodeGroupId ? incrementSpecialNodeGroupId : node.specialNodeGroupId
+ if (this.isOntoEndNode(node.id)){
+ return
+ }
+
this.state.abstraction.relations.map(relation => {
+ let isOnto = this.isRemoteOnto(relation.source, relation.target)
if (relation.source == node.uri) {
if (this.entityExist(relation.target)) {
targetId = this.getId()
@@ -498,29 +543,30 @@ export default class Query extends Component {
label: label,
faldo: this.isFaldoEntity(relation.target),
selected: false,
- suggested: true
+ suggested: true,
+ ontology: isOnto
})
// push suggested link
this.graphState.links.push({
uri: relation.uri,
- type: "link",
+ type: isOnto == "endNode" ? "ontoLink" : "link",
sameStrand: this.nodeHaveStrand(node.uri) && this.nodeHaveStrand(relation.target),
sameRef: this.nodeHaveRef(node.uri) && this.nodeHaveRef(relation.target),
strict: true,
id: linkId,
- label: relation.label,
+ label: isOnto == "endNode" ? this.getOntoLabel(relation.uri) : relation.label,
source: node.id,
target: targetId,
selected: false,
suggested: true,
- directed: true
+ directed: true,
})
incrementSpecialNodeGroupId ? specialNodeGroupId += 1 : specialNodeGroupId = specialNodeGroupId
}
}
}
- if (relation.target == node.uri) {
+ if (relation.target == node.uri && ! isOnto) {
if (this.entityExist(relation.source)) {
sourceId = this.getId()
linkId = this.getId()
@@ -557,7 +603,7 @@ export default class Query extends Component {
target: node.id,
selected: false,
suggested: true,
- directed: true
+ directed: true,
})
incrementSpecialNodeGroupId ? specialNodeGroupId += 1 : specialNodeGroupId = specialNodeGroupId
}
@@ -566,7 +612,7 @@ export default class Query extends Component {
})
// Position
- if (node.faldo) {
+ if (node.faldo && this.showFaldo) {
this.state.abstraction.entities.map(entity => {
if (entity.faldo) {
let new_id = this.getId()
@@ -585,7 +631,7 @@ export default class Query extends Component {
label: entity.label,
faldo: entity.faldo,
selected: false,
- suggested: true
+ suggested: true,
})
// push suggested link
this.graphState.links.push({
@@ -600,7 +646,7 @@ export default class Query extends Component {
target: new_id,
selected: false,
suggested: true,
- directed: true
+ directed: true,
})
incrementSpecialNodeGroupId ? specialNodeGroupId += 1 : specialNodeGroupId = specialNodeGroupId
}
@@ -626,7 +672,9 @@ export default class Query extends Component {
if (link.source.id == node1.id && link.target.id == node2.id) {
newLink = {
uri: link.uri,
- type: ["included_in", "overlap_with"].includes(link.uri) ? "posLink" : "link",
+ // What's the point of this?
+ // type: ["included_in", "overlap_with"].includes(link.uri) ? "posLink" : "link",
+ type: link.type,
sameStrand: this.nodeHaveStrand(node1.uri) && this.nodeHaveStrand(node2.uri),
sameRef: this.nodeHaveRef(node1.uri) && this.nodeHaveRef(node2.uri),
strict: true,
@@ -636,14 +684,16 @@ export default class Query extends Component {
target: node2.id,
selected: false,
suggested: false,
- directed: true
+ directed: link.directed,
}
}
if (link.source.id == node2.id && link.target.id == node1.id) {
newLink = {
uri: link.uri,
- type: ["included_in", "overlap_with"].includes(link.uri) ? "posLink" : "link",
+ // What's the point of this?
+ // type: ["included_in", "overlap_with"].includes(link.uri) ? "posLink" : "link",
+ type: link.type,
sameStrand: this.nodeHaveStrand(node1.uri) && this.nodeHaveStrand(node2.uri),
sameRef: this.nodeHaveRef(node1.uri) && this.nodeHaveRef(node2.uri),
strict: true,
@@ -653,7 +703,7 @@ export default class Query extends Component {
target: node1.id,
selected: false,
suggested: false,
- directed: true
+ directed: link.directed,
}
}
})
@@ -720,7 +770,7 @@ export default class Query extends Component {
handleLinkSelection (clickedLink) {
// Only position link are clickabl
- if (clickedLink.type == "posLink") {
+ if (clickedLink.type == "posLink" || clickedLink.type == "ontoLink") {
// case 1: link is selected, so deselect it
if (clickedLink.selected) {
// Update current and previous
@@ -859,6 +909,7 @@ export default class Query extends Component {
waiting: waiting
})
console.log(this.graphState)
+ ReactTooltip.rebuild();
}
initGraph () {
@@ -983,6 +1034,17 @@ export default class Query extends Component {
this.updateGraphState()
}
+ // Filter Faldo --------------------------
+ handleFilterFaldo (event) {
+ // Toggle filter
+
+ this.showFaldo = !this.showFaldo
+ // Reset suggestion
+ this.removeAllSuggestion()
+ this.insertSuggestion(this.currentSelected)
+ this.updateGraphState()
+ }
+
// Attributes managment -----------------------
toggleVisibility (event) {
this.graphState.attr.map(attr => {
@@ -1269,6 +1331,27 @@ export default class Query extends Component {
return result
}
+ // Ontology link methods -----------------------------
+
+ handleChangeOntologyType (event) {
+ this.graphState.links.map(link => {
+ if (link.id == event.target.id) {
+ link.uri = event.target.value
+ link.label = this.getOntoLabel(event.target.value)
+ }
+ })
+ this.updateGraphState()
+ }
+
+ getOntoLabel (uri) {
+ let labels = {}
+ labels["http://www.w3.org/2000/01/rdf-schema#subClassOf"] = "is children of"
+ labels["http://www.w3.org/2000/01/rdf-schema#subClassOf*"] = "is descendant of"
+ labels["^http://www.w3.org/2000/01/rdf-schema#subClassOf"] = "is parents of"
+ labels["^http://www.w3.org/2000/01/rdf-schema#subClassOf*"] = "is ancestor of"
+ return labels[uri]
+ }
+
// ------------------------------------------------
// Preview results and Launch query buttons -------
@@ -1438,15 +1521,27 @@ export default class Query extends Component {
let visualizationDiv
let uriLabelBoxes
let AttributeBoxes
+ let isOnto
let linkView
let previewButton
+ let faldoButton
let launchQueryButton
let removeButton
let graphFilters
+ let tooltips = (
+
+ Mark attribute as a form attribute
+ Link this attribute to another
+ Show all values, including empty values.
+ Exclude categories, instead of including
+ Display attribute value in the results
+
+ )
if (!this.state.waiting) {
// attribute boxes (right view) only for node
if (this.currentSelected) {
+ isOnto = this.isOntoNode(this.currentSelected.id)
AttributeBoxes = this.state.graphState.attr.map(attribute => {
if (attribute.nodeId == this.currentSelected.id && this.currentSelected.type == "node") {
return (
@@ -1470,6 +1565,8 @@ export default class Query extends Component {
handleFilterDateValue={p => this.handleFilterDateValue(p)}
handleDateFilter={p => this.handleDateFilter(p)}
config={this.state.config}
+ isOnto={isOnto}
+ entityUri={this.currentSelected.uri}
/>
)
}
@@ -1498,6 +1595,24 @@ export default class Query extends Component {
nodesHaveStrands={p => this.nodesHaveStrands(p)}
/>
}
+
+ if (this.currentSelected.type == "ontoLink") {
+
+ let link = Object.assign(this.currentSelected)
+ this.state.graphState.nodes.map(node => {
+ if (node.id == this.currentSelected.target) {
+ link.target = node
+ }
+ if (node.id == this.currentSelected.source) {
+ link.source = node
+ }
+ })
+
+ linkView = this.handleChangeOntologyType(p)}
+ />
+ }
}
// visualization (left view)
@@ -1531,6 +1646,12 @@ export default class Query extends Component {
)
}
+ faldoButton = (
+
+
+
+ )
+
// Filters
graphFilters = (
Query Builder
-
+
{graphFilters}
-
+
+ {faldoButton}
+
+
{removeButton}
@@ -1578,6 +1702,7 @@ export default class Query extends Component {
{uriLabelBoxes}
{AttributeBoxes}
{linkView}
+ {tooltips}
diff --git a/askomics/react/src/routes/query/visualization.jsx b/askomics/react/src/routes/query/visualization.jsx
index 90ee0ef0..c2a62360 100644
--- a/askomics/react/src/routes/query/visualization.jsx
+++ b/askomics/react/src/routes/query/visualization.jsx
@@ -169,7 +169,7 @@ export default class Visualization extends Component {
link.suggested ? ctx.setLineDash([this.lineWidth, this.lineWidth]) : ctx.setLineDash([])
let greenArray = ["included_in", "overlap_with"]
- let unselectedColor = greenArray.indexOf(link.uri) >= 0 ? this.colorGreen : this.colorGrey
+ let unselectedColor = greenArray.indexOf(link.uri) >= 0 || link.type == "ontoLink" ? this.colorGreen : this.colorGrey
let unselectedColorText = greenArray.indexOf(link.uri) >= 0 ? this.colorGreen : this.colorDarkGrey
ctx.strokeStyle = link.selected ? this.colorFirebrick : unselectedColor
diff --git a/askomics/react/src/routes/results/resultsfilestable.jsx b/askomics/react/src/routes/results/resultsfilestable.jsx
index 3bde0dd2..d51aa5b9 100644
--- a/askomics/react/src/routes/results/resultsfilestable.jsx
+++ b/askomics/react/src/routes/results/resultsfilestable.jsx
@@ -28,7 +28,8 @@ export default class ResultsFilesTable extends Component {
status: null,
modalTracebackTitle: "",
modalTracebackContent: "",
- modalTraceback: false
+ modalTraceback: false,
+ console_enabled: false
}
this.utils = new Utils()
this.handleSelection = this.handleSelection.bind(this)
diff --git a/askomics/react/src/routes/sparql/resultstable.jsx b/askomics/react/src/routes/sparql/resultstable.jsx
index f2592ee1..be61dde4 100644
--- a/askomics/react/src/routes/sparql/resultstable.jsx
+++ b/askomics/react/src/routes/sparql/resultstable.jsx
@@ -59,7 +59,7 @@ export default class ResultsTable extends Component {
index: index,
formatter: (cell, row) => {
if (this.utils.isUrl(cell)) {
- return
{this.utils.splitUrl(cell)}
+ return
{this.utils.splitUrl(cell)}
}
return cell
},
diff --git a/askomics/react/src/routes/upload/filestable.jsx b/askomics/react/src/routes/upload/filestable.jsx
index 7b399c19..2864fcb9 100644
--- a/askomics/react/src/routes/upload/filestable.jsx
+++ b/askomics/react/src/routes/upload/filestable.jsx
@@ -3,6 +3,7 @@ import axios from 'axios'
import BootstrapTable from 'react-bootstrap-table-next'
import paginationFactory from 'react-bootstrap-table2-paginator'
import cellEditFactory from 'react-bootstrap-table2-editor'
+import {Badge} from 'reactstrap'
import WaitingDiv from '../../components/waiting'
import Utils from '../../classes/utils'
import PropTypes from 'prop-types'
@@ -88,6 +89,20 @@ export default class FilesTable extends Component {
formatter: (cell, row) => { return this.utils.humanFileSize(cell, true) },
sort: true,
editable: false
+ }, {
+ dataField: 'status',
+ text: 'File status',
+ formatter: (cell, row) => {
+ if (cell == 'downloading') {
+ return
Downloading
+ }
+ if (cell == 'available') {
+ return
Available
+ }
+ return
Error
+ },
+ sort: true,
+ editable: false
}]
let defaultSorted = [{
diff --git a/askomics/react/src/routes/upload/uploadurlform.jsx b/askomics/react/src/routes/upload/uploadurlform.jsx
index 28646b68..ce631744 100644
--- a/askomics/react/src/routes/upload/uploadurlform.jsx
+++ b/askomics/react/src/routes/upload/uploadurlform.jsx
@@ -45,8 +45,8 @@ export default class UploadUrlForm extends Component {
this.setState({
disabled: true,
progressAnimated: true,
- progressValue: 99,
- progressDisplay: "99 %",
+ progressValue: 0,
+ progressDisplay: "0 %",
progressColor: "success"
})
@@ -96,7 +96,7 @@ export default class UploadUrlForm extends Component {
{this.state.progressDisplay}
-
Upload
+
Request upload
diff --git a/askomics/static/about.html b/askomics/static/about.html
index 4b1fc22e..8c8b1402 100644
--- a/askomics/static/about.html
+++ b/askomics/static/about.html
@@ -12,7 +12,7 @@ What is AskOmics?
Visit askomics.org to learn how to use and deploy AskOmics.
-Usefull links
+Useful links
Docs
diff --git a/askomics/static/css/askomics.css b/askomics/static/css/askomics.css
index 7f7bb842..2d6a4065 100644
--- a/askomics/static/css/askomics.css
+++ b/askomics/static/css/askomics.css
@@ -276,4 +276,37 @@ button.input-with-icon {
display: block;
}
-/***********************************************************************/
\ No newline at end of file
+/***********************************************************************/
+
+
+.react-autosuggest__suggestions-container--open {
+ background-clip: padding-box;
+ background-color: #fff;
+ border: 1px solid rgba(0,0,0,0.15);
+ bottom: auto;
+ box-shadow: 0 6px 12px rgba(0,0,0,0.175);
+ display: block;
+ font-size: 14px;
+ list-style: none;
+ padding: 1px;
+ position: absolute;
+ text-align: left;
+ z-index: 20000;
+}
+
+.react-autosuggest__suggestions-list {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.react-autosuggest__suggestion {
+ cursor: pointer;
+ padding: 10px;
+ min-width: 100px;
+}
+
+.react-autosuggest__suggestion--highlighted {
+ background-color: #0356fc;
+ color: #fff;
+}
diff --git a/askomics/tasks.py b/askomics/tasks.py
index e0059151..ee048d3e 100644
--- a/askomics/tasks.py
+++ b/askomics/tasks.py
@@ -44,10 +44,10 @@ def integrate(self, session, data, host_url):
error: True if error, else False
errorMessage: the error message of error, else an empty string
"""
- files_handler = FilesHandler(app, session, host_url=host_url, external_endpoint=data["externalEndpoint"], custom_uri=data["customUri"])
+ files_handler = FilesHandler(app, session, host_url=host_url, external_endpoint=data["externalEndpoint"], custom_uri=data["customUri"], external_graph=data['externalGraph'])
files_handler.handle_files([data["fileId"], ])
- public = data.get("public", False) if session["user"]["admin"] else False
+ public = (data.get("public", False) if session["user"]["admin"] else False) or app.iniconfig.getboolean("askomics", "single_tenant", fallback=False)
for file in files_handler.files:
@@ -151,20 +151,28 @@ def query(self, session, info):
info["celery_id"] = self.request.id
result = Result(app, session, info, force_no_db=True)
+ query = SparqlQuery(app, session, info["graph_state"])
+ query.build_query_from_json(preview=False, for_editor=False)
+ federated = query.is_federated()
+ result.populate_db(query.graphs, query.endpoints)
+
+ info["query"] = query.sparql
+ info["graphs"] = query.graphs
+ info["endpoints"] = query.endpoints
+ info["federated"] = federated
+ info["selects"] = query.selects
+
# Save job in database database
result.set_celery_id(self.request.id)
result.update_db_status("started", update_celery=True, update_date=True)
# launch query
- query = SparqlQuery(app, session, info["graph_state"])
- query.build_query_from_json(for_editor=False)
-
- headers = query.selects
+ headers = info["selects"]
results = []
- if query.graphs:
- query_launcher = SparqlQueryLauncher(app, session, get_result_query=True, federated=query.federated, endpoints=query.endpoints)
- headers, results = query_launcher.process_query(query.sparql, isql_api=True)
+ if info["graphs"] or app.iniconfig.getboolean("askomics", "single_tenant", fallback=False):
+ query_launcher = SparqlQueryLauncher(app, session, get_result_query=True, federated=info["federated"], endpoints=info["endpoints"])
+ headers, results = query_launcher.process_query(info["query"], isql_api=True)
# write result to a file
file_size = result.save_result_in_file(headers, results)
@@ -288,3 +296,18 @@ def send_mail_new_user(self, session, user):
"""
local_auth = LocalAuth(app, session)
local_auth.send_mail_to_new_user(user)
+
+
+@celery.task(bind=True, name="download_file")
+def download_file(self, session, url):
+ """Send a mail to the new user
+
+ Parameters
+ ----------
+ session : dict
+ AskOmics session
+ user : dict
+ New user
+ """
+ files = FilesHandler(app, session)
+ files.download_url(url, download_file.request.id)
diff --git a/config/askomics.ini.template b/config/askomics.ini.template
index e22d88a2..5e75e378 100644
--- a/config/askomics.ini.template
+++ b/config/askomics.ini.template
@@ -12,9 +12,9 @@ debug = false
debug_ttl = false
# If Askomics is running under a sub path (like http://example.org/askomics, subpath is /askomics)
-#reverse_proxy_path =
+#reverse_proxy_path =
-# subtitle =
+# subtitle =
footer_message = Welcome to AskOmics!
#front_message=
@@ -61,12 +61,12 @@ instance_url = http://localhost:5000
# If set, host, port and sender are mandatory.
# user and password are optional
# connection: starttls or nothing
-#smtp_host =
-#smtp_port =
-#smtp_sender =
-#smtp_user =
-#smtp_password =
-#smtp_connection =
+#smtp_host =
+#smtp_port =
+#smtp_sender =
+#smtp_user =
+#smtp_password =
+#smtp_connection =
# LDAP
ldap_auth = false
@@ -80,16 +80,20 @@ ldap_username_attribute = uid
ldap_first_name_attribute = givenName
ldap_surname_attribute = sn
ldap_mail_attribute = mail
-#ldap_password_reset_link =
-#ldap_account_link =
+#ldap_password_reset_link =
+#ldap_account_link =
+
+# Max results returned for autocompletion
+autocomplete_max_results = 10
+
[triplestore]
# name of the triplestore, can be virtuoso or fuseki
triplestore = virtuoso
# Sparql endpoint
-endpoint = http://localhost:8890/sparql
+endpoint = http://localhost:8890/sparql-auth
# Sparql updatepoint
-updatepoint = http://localhost:8890/sparql
+updatepoint = http://localhost:8890/sparql-auth
# Isql API
# If triplestore is virtuoso, set the (optional) isql api for fastest graph deletion
#isqlapi = http://localhost:5050
@@ -98,8 +102,10 @@ updatepoint = http://localhost:8890/sparql
# Triplestore credentials
username = dba
password = dba
+# Http auth method for sparqlwrapper: basic or digest (virtuoso require digest)
+http_auth = digest
# If the triplesotre and askomics are on different network, the loadurl is askomics url accessible by the triplesotre
-# load_url =
+# load_url =
upload_method = load
# Number of triple to integrate in one request
chunk_size = 60000
@@ -122,6 +128,10 @@ preview_limit = 25
# Triplestore max rows limit
# result_set_max_rows = 10000
+# Single tenant means all graphs are public
+# All queries are launched on all graphes (speedup queries)
+single_tenant=False
+
[federation]
# Query engine can be corese or fedx
#query_engine = corese
diff --git a/config/askomics.test.ini b/config/askomics.test.ini
index 5609a185..6d9bd17c 100644
--- a/config/askomics.test.ini
+++ b/config/askomics.test.ini
@@ -11,7 +11,7 @@ result_backend = redis://localhost:6380
debug = false
debug_ttl = false
-# subtitle =
+# subtitle =
footer_message = Test
#front_message=
@@ -55,12 +55,12 @@ instance_url = http://localhost:5000
# If set, host, port and sender are mandatory.
# user and password are optional
# connection: starttls or nothing
-#smtp_host =
-#smtp_port =
-#smtp_sender =
-#smtp_user =
-#smtp_password =
-#smtp_connection =
+#smtp_host =
+#smtp_port =
+#smtp_sender =
+#smtp_user =
+#smtp_password =
+#smtp_connection =
# LDAP
ldap_auth = false
@@ -74,16 +74,17 @@ ldap_username_attribute = uid
ldap_first_name_attribute = givenName
ldap_surname_attribute = sn
ldap_mail_attribute = mail
-#ldap_password_reset_link =
-#ldap_account_link =
+#ldap_password_reset_link =
+#ldap_account_link =
+autocomplete_max_results = 20
[triplestore]
# name of the triplestore, can be virtuoso or fuseki
triplestore = virtuoso
# Sparql endpoint
-endpoint = http://localhost:8891/sparql
+endpoint = http://localhost:8891/sparql-auth
# Sparql updatepoint
-updatepoint = http://localhost:8891/sparql
+updatepoint = http://localhost:8891/sparql-auth
# Isql API
# If triplestore is virtuoso, set the (optional) isql api for fastest graph deletion
isqlapi = http://localhost:5051
@@ -92,8 +93,10 @@ isqlapi = http://localhost:5051
# Triplestore credentials
username = dba
password = dba
-# If the triplesotre and askomics are on different network, the loadurl is askomics url accessible by the triplesotre
-# load_url =
+# Http auth method for sparqlwrapper: basic or digest (virtuoso require digest)
+http_auth = digest
+# If the triplestore and askomics are on different network, the loadurl is askomics url accessible by the triplesotre
+# load_url =
upload_method = insert
# Number of triple to integrate in one request
chunk_size = 60000
@@ -116,6 +119,8 @@ preview_limit = 25
# Triplestore max rows limit
result_set_max_rows = 10000
+single_tenant=False
+
[federation]
# Query engine can be corese or fedx
query_engine = corese
@@ -128,3 +133,5 @@ local_endpoint=http://askomics-host:8891/sparql
# Sentry dsn to report python and js errors in a sentry instance
# server_dsn = https://00000000000000000000000000000000@exemple.org/1
# frontend_dsn = https://00000000000000000000000000000000@exemple.org/2
+
+# Max results returned for autocompletion
diff --git a/docker/Dockerfile b/docker/Dockerfile
index c8b7ce9f..10c4902e 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,4 +1,4 @@
-FROM askomics/flaskomics-base:4.0.0-alpine3.9 AS builder
+FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.13 AS builder
MAINTAINER "Xavier Garnier
"
COPY . /askomics
@@ -7,10 +7,10 @@ WORKDIR /askomics
RUN make clean-config fast-install build
# Final image
-FROM alpine:3.9
+FROM alpine:3.13
WORKDIR /askomics
-RUN apk add --no-cache make python3 bash git libc-dev libstdc++ nodejs nodejs-npm openldap-dev
+RUN apk add --no-cache make python3 bash git libc-dev libstdc++ nodejs-current npm openldap-dev
COPY --from=builder /askomics .
EXPOSE 5000
diff --git a/docker/DockerfileAll b/docker/DockerfileAll
index fb9709cc..65224511 100644
--- a/docker/DockerfileAll
+++ b/docker/DockerfileAll
@@ -1,5 +1,5 @@
# Build AskOmics
-FROM askomics/flaskomics-base:4.0.0-alpine3.9 AS askomics_builder
+FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.13 AS askomics_builder
MAINTAINER "Xavier Garnier "
COPY . /askomics
@@ -14,7 +14,7 @@ FROM xgaia/corese:20.6.11 AS corese_builder
FROM askomics/virtuoso:7.2.5.1 AS virtuoso_builder
# Final image
-FROM alpine:3.8
+FROM alpine:3.13
ENV MODE="prod" \
NTASKS="5" \
@@ -35,7 +35,7 @@ RUN apk add --no-cache openssl py-pip && \
apk --no-cache add --update openjdk11 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community && \
mkdir /corese && \
apk add --no-cache redis sqlite && \
- apk add --no-cache make python3 bash git libc-dev libstdc++ nodejs nodejs-npm openldap-dev
+ apk add --no-cache make python3 bash git libc-dev libstdc++ nodejs-current nodejs openldap-dev
COPY --from=virtuoso_builder /usr/local/virtuoso-opensource /usr/local/virtuoso-opensource
COPY --from=virtuoso_builder /virtuoso /virtuoso
diff --git a/docker/DockerfileCelery b/docker/DockerfileCelery
index d2df8601..db08157a 100644
--- a/docker/DockerfileCelery
+++ b/docker/DockerfileCelery
@@ -1,4 +1,4 @@
-FROM askomics/flaskomics-base:4.0.0-alpine3.9 AS builder
+FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.13 AS builder
MAINTAINER "Xavier Garnier "
COPY . /askomics
@@ -7,7 +7,7 @@ WORKDIR /askomics
RUN make clean-config fast-install
# Final image
-FROM alpine:3.9
+FROM alpine:3.13
WORKDIR /askomics
RUN apk add --no-cache make python3 bash git libc-dev libstdc++ openldap-dev
diff --git a/docs/abstraction.md b/docs/abstraction.md
index dec0278b..208ff7ad 100644
--- a/docs/abstraction.md
+++ b/docs/abstraction.md
@@ -1,6 +1,10 @@
During integration of TSV/CSV, GFF and BED files, AskOmics create RDF triples that describe the data. This set of triple are called *Abstraction*. *Abstraction* is a set of RDF triples who describes the data. This triples define *Entities*, *Attributes* and *Relations*. Abstraction is used to build the *Query builder*.
-Raw RDF can be integrated into AskOmics. In this case, abstraction have to be built manually. The following documentation explain how to write an AskOmics abstraction in turtle format.
+Raw RDF can be integrated into AskOmics. In this case, abstraction have to be built manually. The following documentation explain how to write manually write an AskOmics abstraction in turtle format.
+
+!!! warning
+ Starting from 4.4, attributes & relations are defined using blank nodes, to avoid overriding information
+ They are linked to the correct node using askomics:uri
# Namespaces
@@ -10,16 +14,19 @@ AskOmics use the following namespaces.
PREFIX :
PREFIX askomics:
PREFIX dc:
+PREFIX dcat:
PREFIX faldo:
PREFIX owl:
PREFIX prov:
PREFIX rdf:
PREFIX rdfs:
+PREFIX skos:
PREFIX xsd:
+PREFIX dcat:
```
-!!! info
+!!! note "Info"
Namespaces `:` and `askomics:` are defined in the AskOmics config file (`config/askomics.ini`)
# Entity
@@ -31,12 +38,17 @@ The entity is a class. In the query builder, it is represented with a graph node
:EntityName rdf:type owl:Class .
:EntityName rdf:type askomics:startPoint .
:EntityName rdfs:label "EntityName" .
+# Optional (use if no label)
+:EntityName askomics:instancesHaveNoLabels true .
```
-!!! info
+!!! note "Info"
`:EntityName rdf:type :startPoint` is not mandatory. If the entity have this triple, a query can be started with this this node.
+!!! note "Info"
+ `:EntityName rdfs:label "EntityName"` is optional. If your entity has no label, you can use `:EntityName askomics:instancesHaveNoLabels true` instead. In the query view, the label tab will not be displayed.
+
# Attributes
Attributes are linked to an entity. 3 types of attributes are used in AskOmics: *numeric*, *text* and *category*.
@@ -44,19 +56,21 @@ Attributes are linked to an entity. 3 types of attributes are used in AskOmics:
## Numeric
```turtle
-:numeric_attribute rdf:type owl:DatatypeProperty .
-:numeric_attribute rdfs:label "numeric_attribute" .
-:numeric_attribute rdfs:domain :EntityName .
-:numeric_attribute rdfs:range xsd:decimal .
+_:blank rdf:type owl:DatatypeProperty .
+_:blank rdfs:label "numeric_attribute" .
+_:blank rdfs:domain :EntityName .
+_:blank rdfs:range xsd:decimal .
+_:blank askomics:uri :numeric_attribute_uri
```
## Text
```turtle
-:text_attribute rdf:type owl:DatatypeProperty .
-:text_attribute rdfs:label "text_attribute" .
-:text_attribute rdfs:domain :EntityName .
-:text_attribute rdfs:range xsd:string .
+_:blank rdf:type owl:DatatypeProperty .
+_:blank rdfs:label "text_attribute" .
+_:blank rdfs:domain :EntityName .
+_:blank rdfs:range xsd:string .
+_:blank askomics:uri :text_attribute_uri
```
## Category
@@ -64,11 +78,13 @@ Attributes are linked to an entity. 3 types of attributes are used in AskOmics:
Category is an attribute that have a limited number of values. All values of the category are stored in the abstraction. The ttl below represent a category `category_attribute` who can takes 2 values: `value_1` and `value_2`.
```turtle
-:category_attribute rdf:type owl:ObjectProperty .
-:category_attribute rdf:type askomics:AskomicsCategory .
-:category_attribute rdfs:label "category_attribute" .
-:category_attribute rdfs:domain :EntityName .
-:category_attribute rdfs:range :category_attributeCategory .
+_:blank rdf:type owl:ObjectProperty .
+_:blank rdf:type askomics:AskomicsCategory .
+_:blank rdfs:label "category_attribute" .
+_:blank rdfs:domain :EntityName .
+_:blank rdfs:range :category_attributeCategory .
+_:blank askomics:uri :category_attribute_uri
+
:category_attributeCategory askomics:category :value_1 .
:category_attributeCategory askomics:category :value_2 .
@@ -101,12 +117,13 @@ Four FALDO attributes are supported by AskOmics: reference, strand, start and en
A faldo:reference attribute derive from a Category attribute.
```turtle
-:reference_attribute rdf:type askomics:faldoReference .
-:reference_attribute rdf:type askomics:AskomicsCategory .
-:reference_attribute rdf:type owl:ObjectProperty .
-:reference_attribute rdfs:label "reference_attribute" .
-:reference_attribute rdfs:domain :EntityName .
-:reference_attribute rdfs:range :reference_attributeCategory.
+_:blank rdf:type askomics:faldoReference .
+_:blank rdf:type askomics:AskomicsCategory .
+_:blank rdf:type owl:ObjectProperty .
+_:blank rdfs:label "reference_attribute" .
+_:blank rdfs:domain :EntityName .
+_:blank rdfs:range :reference_attributeCategory.
+_:blank askomics:uri :reference_attribute
```
### faldo:strand
@@ -114,12 +131,13 @@ A faldo:reference attribute derive from a Category attribute.
faldo:strand is also a category.
```turtle
-:strand_attribute rdf:type askomics:faldoStrand .
-:strand_attribute rdf:type askomics:AskomicsCategory .
-:strand_attribute rdf:type owl:ObjectProperty .
-:strand_attribute rdfs:label "strand_attribute" .
-:strand_attribute rdfs:domain :EntityName .
-:strand_attribute rdfs:range :strand_attributeCategory.
+_:blank rdf:type askomics:faldoStrand .
+_:blank rdf:type askomics:AskomicsCategory .
+_:blank rdf:type owl:ObjectProperty .
+_:blank rdfs:label "strand_attribute" .
+_:blank rdfs:domain :EntityName .
+_:blank rdfs:range :strand_attributeCategory.
+_:blank askomics:uri :strand_attribute
```
### faldo:start and faldo:end
@@ -127,29 +145,102 @@ faldo:strand is also a category.
faldo:start and faldo:end are numeric attributes.
```turtle
-:start_attribute rdf:type askomics:faldoStart .
-:start_attribute rdf:type owl:DatatypeProperty .
-:start_attribute rdfs:label "start_attribute" .
-:start_attribute rdfs:domain :EntityName .
-:start_attribute rdfs:range xsd:decimal .
+_:blank rdf:type askomics:faldoStart .
+_:blank rdf:type owl:DatatypeProperty .
+_:blank rdfs:label "start_attribute" .
+_:blank rdfs:domain :EntityName .
+_:blank rdfs:range xsd:decimal .
+_:blank askomics_uri :start_attribute
```
```turtle
-:end_attribute rdf:type askomics:faldoEnd .
-:end_attribute rdf:type owl:DatatypeProperty .
-:end_attribute rdfs:label "end_attribute" .
-:end_attribute rdfs:domain :EntityName .
-:end_attribute rdfs:range xsd:decimal .
+_:blank rdf:type askomics:faldoEnd .
+_:blank rdf:type owl:DatatypeProperty .
+_:blank rdfs:label "end_attribute" .
+_:blank rdfs:domain :EntityName .
+_:blank rdfs:range xsd:decimal .
+_:blank askomics:uri :end_attribute
```
# Relations
-Entities are linked between them with relations. Relations are displayed with arrows between nodes on the query builder. The following turtle explain how relations are described.
+Entities are linked between them with relations. Relations are displayed with arrows between nodes on the query builder. The following turtle explain how relations are described. To avoid overwriting information, relations are described using a blank node. The relation `:RelationExample`, linking `EntitySource` to `EntityTarget`, with the label *relation_example*, will be defined as follows:
+
+```turtle
+_:blank askomics:uri :RelationExample .
+_:blank a askomics:AskomicsRelation .
+_:blank a owl:ObjectProperty .
+_:blank rdfs:label "relation_example" .
+_:blank rdfs:domain :EntitySource .
+_:blank rdfs:range :EntityTarget .
+# Optional information for future-proofing
+_:blank dcat:endpointURL .
+_:blank dcat:dataset .
+```
+
+# Federation
+
+To describe a remote dataset, you can either fill out the "Distant endpoint" and optionally the "Distant graph" fields when integrating an RDF dataset, or you could add description triples in your dataset, as follows:
+
+```turtle
+_:blank ns1:atLocation "https://my_remote_endpoint/sparql" .
+_:blank dcat:Dataset .
+```
+
+# Ontologies
+
+Ontologies needs to be are defined as follows:
+
+```turtle
+ rdf:type askomics:ontology .
+ rdf:type owl:Ontology .
+:EntityName rdfs:label "OntologyLabel" .
+```
+
+!!! note "Info"
+ Make sure to use `rdfs:label`, even if your classes use another type of label.
+
+You will then need to add any relations and attributes using blank nodes:
+
+```turtle
+# SubCLassOf relation
+_:blank1 a askomics:AskomicsRelation .
+_:blank1 askomics:uri rdfs:subClassOf .
+_:blank1 rdfs:label "subClassOf" .
+_:blank1 rdfs:domain .
+_:blank1 rdfs:range .
+
+# Ontology attribute 'taxon rank'
+_:blank2 a owl:DatatypeProperty .
+_:blank2 askomics:uri .
+_:blank2 rdfs:label "Taxon rank" .
+_:blank2 rdfs:domain .
+_:blank2 rdfs:range xsd:string .
+```
+
+With these triples, your ontology will appears in the graph view.
+You can then either add your classes directly, or refer to an external endpoint / graph
+
+## Adding the classes directly
+
+Here is an example of an ontological class:
+
+```turtle
+ rdf:type owl:Class .
+ rdfs:subClassOf .
+ "order" .
+ skos:prefLabel "OntologyLabel" .
+```
+
+!!! note "Info"
+ The label does not need to be `rdfs:label`, but you will need to specify the correct label in the UI.
+
+## Using federated queries
+
+If instead you have access to a remote SPARQL endpoint, you can indicate it here:
```turtle
-:relation_example a askomics:AskomicsRelation .
-:relation_example a owl:ObjectProperty .
-:relation_example rdfs:label "relation_example" .
-:relation_example rdfs:domain :EntityName .
-:relation_example rdfs:range :EntityName_2 .
+_:blank ns1:atLocation "https://my_remote_endpoint/sparql" .
+# Optional: Set a specific graph for remote queries
+_:blank dcat:Dataset .
```
diff --git a/docs/ci.md b/docs/ci.md
index 7fe6b072..1b201d19 100644
--- a/docs/ci.md
+++ b/docs/ci.md
@@ -1,4 +1,4 @@
-AskOmics continuous integration is composed of code linting and unit tests on the Python API. CI is launched automaticaly on the [askomics](https://github.com/askomics/flaskomics) repository on every pull requests. No PR will be merged if the CI fail.
+AskOmics continuous integration includes code linting and unit tests on the Python API. CI is launched automaticaly on the [askomics](https://github.com/askomics/flaskomics) repository on every pull requests. No PR will be merged if the CI fail.
# Setup CI environment
diff --git a/docs/cli.md b/docs/cli.md
new file mode 100644
index 00000000..3fffd0bb
--- /dev/null
+++ b/docs/cli.md
@@ -0,0 +1,13 @@
+Starting from release 4.3.0, a CLI is available with the [askoclics](https://github.com/askomics/askoclics) python package.
+This CLI relies on the AskOmics **API Key**, found in your Account management tab.
+
+The main goal of the CLI is to help automatize data upload and integration into an existing AskOmics instance.
+
+Both the python package and the bash command line currently include the following features:
+
+- File management (Upload, list, preview, integrate, delete)
+- Dataset management (List and delete)
+- Results management (List, preview results, download results, get sparql query, and delete)
+- SPARQL management (Send SPARQL query)
+
+This library is currently a work in progress.
diff --git a/docs/configure.md b/docs/configure.md
index cc138e92..31132d21 100644
--- a/docs/configure.md
+++ b/docs/configure.md
@@ -29,7 +29,7 @@ All AskOmics configuration is set in `config/askomics.ini` files. When AskOmics
- `disable_integration` (`true` or `false`): Disable integration to non admin users
- `protect_public` (`true` or `false`): Public datasets and queries are visible only for logged users
- `enable_sparql_console`(`true` or `false`): Allow non-admin logged users to use the sparql console. **This is unsafe.**
- - `quota` (size): Default quota for new users
+ - `quota` (size): Default quota for new users (can be customized individually later)
- `github` (url): Github repository URL
- `instance_url` (url): Instance URL. Used to send link by email when user reset his password
- `smtp_host` (url): SMTP host url
@@ -51,7 +51,7 @@ All AskOmics configuration is set in `config/askomics.ini` files. When AskOmics
- `ldap_mail_attribute` (string): Mail attribute
- `ldap_password_reset_link` (url): Link to manage the LDAP password
- `ldap_account_link` (url): Link to the LDAP account manager
-
+ - `autocomplete_max_results` (int): Max results queries by autocompletion
- `virtuoso`
@@ -73,6 +73,7 @@ All AskOmics configuration is set in `config/askomics.ini` files. When AskOmics
- `namespace_internal` (url): AskOmics namespace for internal triples. Correspond to the `askomics:` prefix. You should change this to your instance url if you want your URIs to be resolved.
- `preview_limit` (int): Number of line to be previewed in the results page
- `result_set_max_rows` (int): Triplestore max row. Must be the same as SPARQL[ResultSetMaxRows] in virtuoso.ini config
+ - `single_tenant` (bool): Enable [single tenant mode](/manage/#single-tenant-mode)
- `federation`
diff --git a/docs/console.md b/docs/console.md
new file mode 100644
index 00000000..59993c7e
--- /dev/null
+++ b/docs/console.md
@@ -0,0 +1,51 @@
+A SPARQL console is available through AskOmics, allowing you to send direct SPARQL queries to the endpoint.
+
+!!! warning
+ The console access is restricted to **logged users**
+
+!!! warning
+ The default AskOmics configuration restrict SPARQL edition and query to the administrators.
+ This can be disabled with the *enable_sparql_console* configuration option.
+
+![SPARQL query generated by AskOmics](img/sparql.png){: .center}
+
+You can reach this console in two ways:
+
+# Console access
+
+- By clicking SPARQL of an existing result in the *Results* page
+ - The console will be pre-filled with the generated SPARQL query of the result
+- Simply heading to the "/sparql" URL
+ - The console will be pre-filled with a default SPARQL query
+
+# Editing your query
+
+You can edit the SPARQL query through the console to customize your query.
+
+## Advanced options
+
+The **Advanced options** tab allows you to customize *how* the query will be sent.
+Namely, you will be able to select which endpoints and datasets the query will use, allowing you to fine-tune the query
+
+- For example, you can exclude some datasets to restrict the results.
+
+!!! note "Info"
+ When accessing the console through the "Results" page, the datasets of interest (relevant to the query) will already be selected. Make sure to customize the selection if you modify the query.
+
+!!! note "Info"
+ When accessing the console directly, all datasets will be selected (which can increase query time)
+
+# Launching query
+
+If you have **editing privileges** (either as an administrator, or through the configuration key), you will be able to either preview or save the query, much like a "normal" query.
+
+If you save the query, it will appears as a normal result in the "Results" tab. The basic functionalities (templates, download) will be available.
+
+!!! warning
+ The Redo button will be disabled for results created from the console
+
+!!! warning
+ The generated *template* will redirect to the SPARQL console. It means
+
+ - Non-logged users will not be able to use it
+ - Only logged users with **editing privileges** will be able to launch the query
diff --git a/docs/contribute.md b/docs/contribute.md
index 4e8ec20c..82b9a446 100644
--- a/docs/contribute.md
+++ b/docs/contribute.md
@@ -14,7 +14,7 @@ git checkout -b my_new_feature
Commit and push your modification to your [fork](https://help.github.com/articles/pushing-to-a-remote/). If your changes modify code, please ensure that is conform to [AskOmics style](#coding-style-guidlines)
-Write tests for your changes, and make sure that they [passes](dev-deployment.md#launch-continuous-integration-locally).
+Write tests for your changes, and make sure that they [passe](dev-deployment.md#launch-continuous-integration-locally).
Open a pull request against the `dev` branch of flaskomics. The message of your pull request should describe your modifications (why and how).
@@ -29,10 +29,10 @@ Ensure all user-enterable strings are unicode capable. Use only English language
### Python
-We follow [PEP-8](https://www.python.org/dev/peps/pep-0008/), with particular emphasis on the parts about knowing when to be inconsistent, and readability being the ultimate goal.
+We follow the [PEP-8](https://www.python.org/dev/peps/pep-0008/) coding convention.
- Whitespace around operators and inside parentheses
-- 4 spaces per indent, spaces, not tabs
+- 4 spaces per indent (not tabs)
- Include docstrings on your modules, class and methods
- Avoid from module import \*. It can cause name collisions that are tedious to track down.
- Class should be in `CamelCase`, methods and variables in `lowercase_with_underscore`
diff --git a/docs/data.md b/docs/data.md
index 655bfd3a..88d8253d 100644
--- a/docs/data.md
+++ b/docs/data.md
@@ -1,5 +1,182 @@
-In this tutorial, we will learn how to build CSV/TSV file for AskOmics.
+# Uploading files
+You can head to the *Files* tab to manage your files. From there, you will be able to upload new files (from your local computer, or a remote endpoint), and delete them.
+!!! warning
+ Deleting files does not delete related datasets.
-*-- Work in progress --*
\ No newline at end of file
+
+# Data visibility
+
+By default, all your uploaded files and datasets are private.
+If you have administrator privileges, you can select the Integrate (Public dataset) button during integration to make the dataset **Public**
+
+!!! note "Info"
+ *Public* datasets will be queriable by any user, including non-logged users. They will not be able to directly access the file, but generated entities will appear on the query graph (and on the starting screen for starting entities).
+
+!!! warning
+ Make sure your public datasets do not contain sensitive information.
+
+
+# CSV/TSV files
+
+AskOmics will integrate a CSV/TSV file using its header. The *type* of each column will be predicted, but you will be able to modify it before integration.
+
+![CSV/TSV integration](img/csv_convert.png){: .center}
+
+
+## Entity (first column)
+
+### Entity URI
+
+The first column of the file will manage the entity itself : the column name will become the entity name, and the values will become the entity's instances **URIs**.
+**URIs** will be created as follows :
+
+* If the value is an **URL**, it will be integrated as it is.
+* If the value is a [CURIE](https://www.w3.org/TR/2010/NOTE-curie-20101216/), it will be transformed into an URL before integration. The list of managed CURIE formats is available [here](https://github.com/askomics/flaskomics/blob/master/askomics/libaskomics/prefix.cc.json).
+* Else, the value will be added to either AskOmics *namespace_data* value, or a custom base URI if specified in the integration form.
+
+!!! Warning
+ Unless you are trying to merge entities, make sure your URIs are unique across **both your personal and public datasets**.
+
+### Entity type
+
+The entity type can either be "starting entity", or "entity". If "starting entity", it may be used to start a query on the AskOmics homepage. Both types will appear as a node in the AskOmics interface.
+
+### Inheritance
+
+The entity can inherit the attributes and relations of a 'mother' entity. Meaning, you will be able to query the sub-entity on both its own, and its 'mother' attributes and relations. The 'mother' entity however will not have access to any 'daughter' attributes or relations.
+
+To setup inheritance, the **column name** needs to be formated as follows:
+- *daughter_entity_name* < *mother_entity_name* (with the < symbol)
+ ie: *Custom_population* < *General population*
+
+!!! Warning
+ The values of this column must be an URI of the *mother* entity
+
+## Entity label (first and second column)
+
+To manually set an entity label, you can set the second column as a *Label* column.
+The values of this column will be used as labels for the generated entities.
+
+!!! Warning
+ If a value is missing in the column, the label will be created based on the entity *URI*. (See below)
+
+If there is no *Label* column, the labels will be generated based on the URIs (The first column).
+
+* If the value is an **URL**, the last non-empty value after a "/" or "#" will be the label.
+* If the value is a **CURIE**, the value after ":" will be the label
+* Else, the raw value is the label
+
+!!! node "Info"
+ For example, a one-column CSV file with the column name "Gene", and the values "gene1", "rdfs:gene2" and "http://myurl/gene3/" will create the entity *Gene*, with two instances labelled *gene1*, *gene2* and *gene3*.
+
+## Attributes
+
+Each column after the first one will be integrated as an *attribute* of the entity. The column name will be set as the name of the attribute.
+Several attribute types are available. The type of an attribute will dictate the way it will be managed in the query form (eg: text field, value selector...)
+
+!!! note 'Info'
+ AskOmics will try to guess the type of a column based on its name and its values. You will be able to set it manually if the auto-detected type doesn't fit.
+
+Attributes can be of the following types :
+
+### Base types
+
+- Numeric: if the values are numeric
+- Text: if all values are strings
+- Date: if all values are dates (using *dateutil.parser*)
+ - *Auto-detected terms are 'date', 'time', 'birthday', 'day'*
+- Category: if there is a limited number of repeated values
+- Boolean: if the values are binary ("True" and "False", or "0" and "1")
+
+!!! Warning
+ If the date format is ambiguous (eg: 01/01/2020), AskOmics will interpret it as *day/month/year*
+
+### FALDO types
+
+If the entity describe a locatable element on a genome (based on the FALDO ontology):
+
+- [Reference](http://biohackathon.org/resource/faldo#reference): chromosome *(Auto-detected terms : 'chr', 'ref', 'scaff')*
+- [Strand](http://biohackathon.org/resource/faldo#StrandedPosition): strand *(Auto-detected terms : 'strand')*
+- Start: start position *(Auto-detected term : 'start', 'begin')*
+- End: end position *(Auto-detected terms : 'end', 'stop')*
+
+!!! Warning
+ To mark an entity as a *FALDO entity*, you need to provide **at least** a *Start* and *End* columns.
+ *Reference* and/or *Strand* are optional, but will enable more specific queries (eg: *Same reference* or *Same strand*)
+
+### Relations
+
+A column can also symbolize a relation to another entity. In this case, the column name must be of the form :
+
+- *relationName@RelatedEntityName* (with the @ symbol)
+ - ie: *Derives_from@Gene*
+
+Two types are available :
+
+- Directed: Relation from this entity to the targeted one *(e.g. A is B’s father, but B is not A’s father)*
+- Symetric: Relation that works in both directions *(e.g. A loves B, and B loves A)*
+
+!!! Warning
+ The content of the column must be URIs of the related entity.
+ *(The related entity and its URIs may be created afterwards)*
+
+Linked URIs must match one of these three formats :
+
+- Full URI
+- CURIE
+- Simple value (the value will transformed into an URI with AskOmics *namespace_data* value)
+
+This link between entities will show up in the query screen, allowing users to query related entities.
+
+!!! note "Info"
+ **All** FALDO entities will be automatically linked with the *included_in* relation, without needing an explicit link.
+ You can still specify your own relations.
+
+!!! Warning
+ For federated queries, the syntax is slightly different. Please refer to [this page](abstraction.md#linking-your-own-data) for more information.
+
+
+# GFF files
+
+!!! Warning
+ Only the *GFF3* format is managed by AskOmics.
+
+Each GFF file can be integrated into several entities. You will be able to select the entities you wish to integrate beforehand. Available entities are the values of the 'type' column of the GFF file. The relations between entities (eg: *Parents* or *Derives_from*) will also be integrated.
+
+![Integration interface for GFF files](img/gff_preview.png){: .center}
+
+Extracted attributes are the following :
+
+- Reference
+- Strand
+- Start
+- End
+- Any attribute in the *attributes* column
+ - *Parents* and *Derives_from* will be converted in relations
+
+!!! note "Info"
+ All entities extracted from GFF files are *FALDO entities*, and will be linked implicitly with the *included_in* relation.
+
+# BED files
+
+Each BED file will be integrated into one entity (the default entity name will be the file name, but it can be customized).
+
+Extracted attributes are the following :
+
+- Reference
+- Strand
+- Start
+- End
+- Score
+
+!!! note "Info"
+ All entities extracted from BED files are *FALDO entities*, and will be linked implicitly with the *included_in* relation.
+
+# TTL Files
+
+You can integrate TTL files in AskOmics, either to integrate your own data, or to enable [federated queries](federation.md) to remote endpoints.
+In both case, you will need to generate or convert your data in AskOmics's format.
+
+This can be done either [manually](abstraction.md) or [automatically](federation.md#auto-generate-external-abstraction-with-abstractor)
diff --git a/docs/dev-deployment.md b/docs/dev-deployment.md
index da82a868..047ed4cf 100644
--- a/docs/dev-deployment.md
+++ b/docs/dev-deployment.md
@@ -1,8 +1,8 @@
-In development mode, AskOmics dependencies can be deployed with docker-compose, but AskOmics have to be running locally, on your dev machine.
+In development mode, you can deploy AskOmics dependencies with docker-compose, but AskOmics itself should be running locally, on your development machine.
# Prerequisites
-Install dev dependencies
+Install AskOmics dependencies
```bash
@@ -28,7 +28,7 @@ apt install -y docker-compose
dnf install -y docker-compose
```
-# Deploy dependencies
+# Deploying dependencies
We provide a `docker-compose` template to run external services used by AskOmics. Clone the [flaskomics-docker-compose](https://github.com/askomics/flaskomics-docker-compose) repository to use it.
diff --git a/docs/docs.md b/docs/docs.md
index ef0ea300..fe752ab6 100644
--- a/docs/docs.md
+++ b/docs/docs.md
@@ -1,6 +1,6 @@
-all the documentation (including what you are reading) can be found [here](https://flaskomics.readthedocs.io). Files are on the [AskOmics repository](https://github.com/askomics/flaskomics/tree/master/docs).
+All the documentation (including what you are reading) can be found [here](https://flaskomics.readthedocs.io). Files are on the [AskOmics repository](https://github.com/askomics/flaskomics/tree/master/docs).
-# Serve doc locally
+# Serve the documentation locally
First, [install askomics in dev mode](/dev-deployment/#install-askomics).
@@ -9,6 +9,6 @@ Then, run
```bash
make serve-doc
```
-Doc will be available at [localhost:8000](localhost:8000)
+The documentation will be available at [localhost:8000](localhost:8000)
-To change port, use `make serve-doc DOCPORT=8001`
+To change the port, use `make serve-doc DOCPORT=8001`
diff --git a/docs/federation.md b/docs/federation.md
index 78478230..a33f294a 100644
--- a/docs/federation.md
+++ b/docs/federation.md
@@ -1,9 +1,11 @@
-A federated query is a query who involve several SPARQL endpoints. AskOmics have his dedicated endpoint for the integrated data, but it is also possible to query external resources.
+A federated query is a query who involve several SPARQL endpoints. AskOmics uses its own dedicated endpoint for the integrated data, but it is also possible to query external resources.
# Define an external endpoint
-The first step is to define an external endpoint. External endpoint have their own description. To Display external entities, AskOmics need the *Abstraction* of the distant endpoint. This external abstraction can be build [automatically](#auto-generate-external-abstraction-with-abstractor) or [manually](abstraction.md).
+The first step is to define an external endpoint. External endpoint have their own description. To display external entities, AskOmics need the *Abstraction* of the distant endpoint.
+
+This external abstraction can be build [automatically](#auto-generate-external-abstraction-with-abstractor) or [manually](abstraction.md).
## Auto-generate external abstraction with abstractor
@@ -15,39 +17,35 @@ abstractor -e -p -o
```
!!! Warning
- Abstractor scan all things in the SPARQL endpoint. You may review the generated file to delete unwanted things.
+ Abstractor scan all things in the SPARQL endpoint. You may wish to review the generated file to delete unwanted things.
## Integrate external abstraction into AskOmics
-Once external endpoint's abstraction is generated, its time to add it into AskOmis. Upload it and integrate it.
-![integrate_external](img/integrate_external.png)
+Once external endpoint's abstraction is generated, its time to add it into AskOmics. Upload it and integrate it.
+![Integrating an external abstraction](img/integrate_external.png){: .center}
!!! Warning
Check that `advanced options` > `Distant endpoint` contain URL of the external endpoint
-
# Query external endpoint
-## Simple query
-
-If AskOmics contain local data, external startpoint are not displayed by default on the start page. Use the `Source` dropdown button to display external entities.
-
-![external_startpoint](img/external_startpoint.png)
-
+## Starting entities
-## Federated query
+If AskOmics already contains local data, external startpoint are not displayed by default on the start page. Use the `Source` dropdown button to display external entities.
+![External startpoint](img/external_startpoint.png){: .center}
-External entities can be interrogate just as local entities. But to link a local dataset to the external endpoint, the file must be structured in a certain way.
+## Linking to your own data
-### Build file
+To link a local dataset to the external endpoint, the file must be structured in a certain way.
-The input file must describe the relation with the external entity. It goes through the header, who must contain the URI of the targeted entity. Content of the file must also be the exact uri of the targeted entity.
+The input file must describe the relation with the external entity. Much like a 'normal' relation, it goes through the header.
+In this case however, instead of simply the entity name, the column name must contain either the full URI or the CURIE of the external entity (e.g *http://nextprot.org/rdf#Gene*). The values of the column must also be the exact uri (full URI or CURIE) of the targeted entity, instead of a raw value.
-For example, the file below describe en entity *gene* who is linked to an external entity *Gene*. The external one is prefixed with the full uri used in the external endpoint. In the content of the file, full URI have to be used to.
+For example, the file below describe en entity *gene* who is linked to an external entity *Gene*. The external one is prefixed with the full uri used in the external endpoint. In the values of the column, you will need to also use the full URI / CURIE.
gene|value|concern@http://nextprot.org/rdf#Gene
@@ -56,6 +54,6 @@ gene_1|0|http://nextprot.org/rdf/gene/ENSG00000169594
gene_2|1|http://nextprot.org/rdf/gene/ENSG00000156603
-### Perform a federated query
+## Perform a federated query
-Once the relations are well described, link between local and distant entities are automatically done by AskOmics. The Query is distributed to the concerned endpoint and results are returned like a classic query.
+Once the relations are described, links between local and distant entities are automatically created by AskOmics. The query is distributed to the external endpoint and results are returned like a classic query.
diff --git a/docs/galaxy.md b/docs/galaxy.md
index d47a062e..9b6fd1f6 100644
--- a/docs/galaxy.md
+++ b/docs/galaxy.md
@@ -1,5 +1,7 @@
Galaxy is a scientific workflow, data integration, and data and analysis persistence and publishing platform that aims to make computational biology accessible to research scientists that do not have computer programming or systems administration experience.
+A Galaxy Training tutorial is available [here](https://training.galaxyproject.org/training-material/topics/transcriptomics/tutorials/rna-seq-analysis-with-askomics-it/tutorial.html)
+
AskOmics can be used with a Galaxy instance in two way:
- With a dedicated AskOmics, import Galaxy datasets into AskOmics and export AskOmics results into Galaxy.
@@ -17,7 +19,7 @@ On your Galaxy account, go to the top menu *User* → *API Keys* and copy your A
On AskOmics, got to Your Name → Account management → **Connect a Galaxy account** and enter the Galaxy URL and API Key.
-![askogalaxy](img/askogalaxy.png)
+![](img/askogalaxy.png){: .center}
Once a Galaxy account is added to AskOmics, you can access to all your Galaxy Datasets from AskOmics.
@@ -51,30 +53,30 @@ Galaxy Interactive Tools (GxITs) are a method to run containerized tools that ar
Search for the AskOmics Interactive tool using the search bar.
-![Search a Galaxy Tool](img/galaxy_search_tool.png)
+![](img/galaxy_search_tool.png){: .center}
Choose input files to automatically upload them into AskOmics
-![Input files](img/galaxy_input_data.png)
+![](img/galaxy_input_data.png){: .center}
!!! Tip
You will able to add more input files later
A dedicated AskOmics instance will be deployed into the Cluster. Wait few minutes and go to the instance using the `click here to display` link.
-![Galaxy](img/galaxy_execute_it.png)
+![](img/galaxy_execute_it.png){: .center}
Once you are into your AskOmics instance, you can see your uploaded files into the Files tab.
-![Galaxy](img/galaxy_askomics_files.png)
+![](img/galaxy_askomics_files.png){: .center}
## Upload additional files
in addition to the Computer and URL buttons, you can now use the galaxy button to import datasets from your galaxy histories
-![Galaxy](img/galaxy_import_from_galaxy.png)
+![](img/galaxy_import_from_galaxy.png){: .center}
## Integrate and Query
@@ -84,7 +86,4 @@ follow the [tutorial](/tutorial#data-integration) to integrate and query your da
Once you have your result, Use the `Send result to Galaxy` to export a TSV file into your last recently used Galaxy history.
-![Galaxy](img/galaxy_history_result.png)
-
-
-
+![](img/galaxy_history_result.png){: .center}
diff --git a/docs/img/askogalaxy.png b/docs/img/askogalaxy.png
index 44dcaa72619fabfe2646f2797600231506f809f5..5dd6e0379e50191323aa06bb58b16b8f03e55c8b 100644
GIT binary patch
literal 15318
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
Ie*X490F#JdEdT%j
literal 15352
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
diff --git a/docs/img/askograph.png b/docs/img/askograph.png
new file mode 100644
index 0000000000000000000000000000000000000000..a898d77c6bd0ad7fc8ee6cbcdb0f0d0a7673f2a0
GIT binary patch
literal 34589
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